feat(antigravity): 自动设置隐私并支持后台手动重试
新增 Antigravity OAuth 隐私设置能力,在账号创建、刷新、导入和后台 Token 刷新路径自动调用 setUserSettings + fetchUserInfo 关闭遥测; 持久化后同步内存 Extra,错误处理改为日志记录。 Made-with: Cursor
This commit is contained in:
parent
0f03393010
commit
c2965c0fb0
@ -267,6 +267,9 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 收集需要异步设置隐私的 Antigravity OAuth 账号
|
||||||
|
var privacyAccounts []*service.Account
|
||||||
|
|
||||||
for i := range dataPayload.Accounts {
|
for i := range dataPayload.Accounts {
|
||||||
item := dataPayload.Accounts[i]
|
item := dataPayload.Accounts[i]
|
||||||
if err := validateDataAccount(item); err != nil {
|
if err := validateDataAccount(item); err != nil {
|
||||||
@ -314,7 +317,8 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest)
|
|||||||
SkipDefaultGroupBind: skipDefaultGroupBind,
|
SkipDefaultGroupBind: skipDefaultGroupBind,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := h.adminService.CreateAccount(ctx, accountInput); err != nil {
|
created, err := h.adminService.CreateAccount(ctx, accountInput)
|
||||||
|
if err != nil {
|
||||||
result.AccountFailed++
|
result.AccountFailed++
|
||||||
result.Errors = append(result.Errors, DataImportError{
|
result.Errors = append(result.Errors, DataImportError{
|
||||||
Kind: "account",
|
Kind: "account",
|
||||||
@ -323,9 +327,30 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest)
|
|||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// 收集 Antigravity OAuth 账号,稍后异步设置隐私
|
||||||
|
if created.Platform == service.PlatformAntigravity && created.Type == service.AccountTypeOAuth {
|
||||||
|
privacyAccounts = append(privacyAccounts, created)
|
||||||
|
}
|
||||||
result.AccountCreated++
|
result.AccountCreated++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 异步设置 Antigravity 隐私,避免大量导入时阻塞请求
|
||||||
|
if len(privacyAccounts) > 0 {
|
||||||
|
adminSvc := h.adminService
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
slog.Error("import_antigravity_privacy_panic", "recover", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
bgCtx := context.Background()
|
||||||
|
for _, acc := range privacyAccounts {
|
||||||
|
adminSvc.ForceAntigravityPrivacy(bgCtx, acc)
|
||||||
|
}
|
||||||
|
slog.Info("import_antigravity_privacy_done", "count", len(privacyAccounts))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -536,6 +537,8 @@ func (h *AccountHandler) Create(c *gin.Context) {
|
|||||||
if execErr != nil {
|
if execErr != nil {
|
||||||
return nil, execErr
|
return nil, execErr
|
||||||
}
|
}
|
||||||
|
// Antigravity OAuth: 新账号直接设置隐私
|
||||||
|
h.adminService.ForceAntigravityPrivacy(ctx, account)
|
||||||
return h.buildAccountResponseWithRuntime(ctx, account), nil
|
return h.buildAccountResponseWithRuntime(ctx, account), nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -883,6 +886,8 @@ func (h *AccountHandler) refreshSingleAccount(ctx context.Context, account *serv
|
|||||||
|
|
||||||
// OpenAI OAuth: 刷新成功后检查并设置 privacy_mode
|
// OpenAI OAuth: 刷新成功后检查并设置 privacy_mode
|
||||||
h.adminService.EnsureOpenAIPrivacy(ctx, updatedAccount)
|
h.adminService.EnsureOpenAIPrivacy(ctx, updatedAccount)
|
||||||
|
// Antigravity OAuth: 刷新成功后检查并设置 privacy_mode
|
||||||
|
h.adminService.EnsureAntigravityPrivacy(ctx, updatedAccount)
|
||||||
|
|
||||||
return updatedAccount, "", nil
|
return updatedAccount, "", nil
|
||||||
}
|
}
|
||||||
@ -1154,6 +1159,8 @@ func (h *AccountHandler) BatchCreate(c *gin.Context) {
|
|||||||
success := 0
|
success := 0
|
||||||
failed := 0
|
failed := 0
|
||||||
results := make([]gin.H, 0, len(req.Accounts))
|
results := make([]gin.H, 0, len(req.Accounts))
|
||||||
|
// 收集需要异步设置隐私的 Antigravity OAuth 账号
|
||||||
|
var privacyAccounts []*service.Account
|
||||||
|
|
||||||
for _, item := range req.Accounts {
|
for _, item := range req.Accounts {
|
||||||
if item.RateMultiplier != nil && *item.RateMultiplier < 0 {
|
if item.RateMultiplier != nil && *item.RateMultiplier < 0 {
|
||||||
@ -1196,6 +1203,10 @@ func (h *AccountHandler) BatchCreate(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// 收集 Antigravity OAuth 账号,稍后异步设置隐私
|
||||||
|
if account.Platform == service.PlatformAntigravity && account.Type == service.AccountTypeOAuth {
|
||||||
|
privacyAccounts = append(privacyAccounts, account)
|
||||||
|
}
|
||||||
success++
|
success++
|
||||||
results = append(results, gin.H{
|
results = append(results, gin.H{
|
||||||
"name": item.Name,
|
"name": item.Name,
|
||||||
@ -1204,6 +1215,22 @@ func (h *AccountHandler) BatchCreate(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 异步设置 Antigravity 隐私,避免批量创建时阻塞请求
|
||||||
|
if len(privacyAccounts) > 0 {
|
||||||
|
adminSvc := h.adminService
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
slog.Error("batch_create_antigravity_privacy_panic", "recover", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
bgCtx := context.Background()
|
||||||
|
for _, acc := range privacyAccounts {
|
||||||
|
adminSvc.ForceAntigravityPrivacy(bgCtx, acc)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
return gin.H{
|
return gin.H{
|
||||||
"success": success,
|
"success": success,
|
||||||
"failed": failed,
|
"failed": failed,
|
||||||
@ -1869,6 +1896,42 @@ func (h *AccountHandler) GetAvailableModels(c *gin.Context) {
|
|||||||
response.Success(c, models)
|
response.Success(c, models)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetPrivacy handles setting privacy for a single Antigravity OAuth account
|
||||||
|
// POST /api/v1/admin/accounts/:id/set-privacy
|
||||||
|
func (h *AccountHandler) SetPrivacy(c *gin.Context) {
|
||||||
|
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, "Invalid account ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
account, err := h.adminService.GetAccount(c.Request.Context(), accountID)
|
||||||
|
if err != nil {
|
||||||
|
response.NotFound(c, "Account not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if account.Platform != service.PlatformAntigravity || account.Type != service.AccountTypeOAuth {
|
||||||
|
response.BadRequest(c, "Only Antigravity OAuth accounts support privacy setting")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mode := h.adminService.ForceAntigravityPrivacy(c.Request.Context(), account)
|
||||||
|
if mode == "" {
|
||||||
|
response.BadRequest(c, "Cannot set privacy: missing access_token")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 从 DB 重新读取以确保返回最新状态
|
||||||
|
updated, err := h.adminService.GetAccount(c.Request.Context(), accountID)
|
||||||
|
if err != nil {
|
||||||
|
// 隐私已设置成功但读取失败,回退到内存更新
|
||||||
|
if account.Extra == nil {
|
||||||
|
account.Extra = make(map[string]any)
|
||||||
|
}
|
||||||
|
account.Extra["privacy_mode"] = mode
|
||||||
|
response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), updated))
|
||||||
|
}
|
||||||
|
|
||||||
// RefreshTier handles refreshing Google One tier for a single account
|
// RefreshTier handles refreshing Google One tier for a single account
|
||||||
// POST /api/v1/admin/accounts/:id/refresh-tier
|
// POST /api/v1/admin/accounts/:id/refresh-tier
|
||||||
func (h *AccountHandler) RefreshTier(c *gin.Context) {
|
func (h *AccountHandler) RefreshTier(c *gin.Context) {
|
||||||
|
|||||||
@ -445,6 +445,14 @@ func (s *stubAdminService) EnsureOpenAIPrivacy(ctx context.Context, account *ser
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *stubAdminService) EnsureAntigravityPrivacy(ctx context.Context, account *service.Account) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubAdminService) ForceAntigravityPrivacy(ctx context.Context, account *service.Account) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (s *stubAdminService) ReplaceUserGroup(ctx context.Context, userID, oldGroupID, newGroupID int64) (*service.ReplaceUserGroupResult, error) {
|
func (s *stubAdminService) ReplaceUserGroup(ctx context.Context, userID, oldGroupID, newGroupID int64) (*service.ReplaceUserGroupResult, error) {
|
||||||
return &service.ReplaceUserGroupResult{MigratedKeys: 0}, nil
|
return &service.ReplaceUserGroupResult{MigratedKeys: 0}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -704,3 +704,139 @@ func (c *Client) FetchAvailableModels(ctx context.Context, accessToken, projectI
|
|||||||
|
|
||||||
return nil, nil, lastErr
|
return nil, nil, lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Privacy API ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// privacyBaseURL 隐私设置 API 仅使用 daily 端点(与 Antigravity 客户端行为一致)
|
||||||
|
const privacyBaseURL = antigravityDailyBaseURL
|
||||||
|
|
||||||
|
// SetUserSettingsRequest setUserSettings 请求体
|
||||||
|
type SetUserSettingsRequest struct {
|
||||||
|
UserSettings map[string]any `json:"user_settings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchUserInfoRequest fetchUserInfo 请求体
|
||||||
|
type FetchUserInfoRequest struct {
|
||||||
|
Project string `json:"project"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchUserInfoResponse fetchUserInfo 响应体
|
||||||
|
type FetchUserInfoResponse struct {
|
||||||
|
UserSettings map[string]any `json:"userSettings,omitempty"`
|
||||||
|
RegionCode string `json:"regionCode,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPrivate 判断隐私是否已设置:userSettings 为空或不含 telemetryEnabled 表示已设置
|
||||||
|
func (r *FetchUserInfoResponse) IsPrivate() bool {
|
||||||
|
if r == nil || r.UserSettings == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
_, hasTelemetry := r.UserSettings["telemetryEnabled"]
|
||||||
|
return !hasTelemetry
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserSettingsResponse setUserSettings 响应体
|
||||||
|
type SetUserSettingsResponse struct {
|
||||||
|
UserSettings map[string]any `json:"userSettings,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSuccess 判断 setUserSettings 是否成功:返回 {"userSettings":{}} 且无 telemetryEnabled
|
||||||
|
func (r *SetUserSettingsResponse) IsSuccess() bool {
|
||||||
|
if r == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// userSettings 为 nil 或空 map 均视为成功
|
||||||
|
if r.UserSettings == nil || len(r.UserSettings) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 如果包含 telemetryEnabled 字段,说明未成功清除
|
||||||
|
_, hasTelemetry := r.UserSettings["telemetryEnabled"]
|
||||||
|
return !hasTelemetry
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserSettings 调用 setUserSettings API 设置用户隐私,返回解析后的响应
|
||||||
|
func (c *Client) SetUserSettings(ctx context.Context, accessToken string) (*SetUserSettingsResponse, error) {
|
||||||
|
// 发送空 user_settings 以清除隐私设置
|
||||||
|
payload := SetUserSettingsRequest{UserSettings: map[string]any{}}
|
||||||
|
bodyBytes, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("序列化请求失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
apiURL := privacyBaseURL + "/v1internal:setUserSettings"
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(bodyBytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "*/*")
|
||||||
|
req.Header.Set("User-Agent", GetUserAgent())
|
||||||
|
req.Header.Set("X-Goog-Api-Client", "gl-node/22.21.1")
|
||||||
|
req.Host = "daily-cloudcode-pa.googleapis.com"
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("setUserSettings 请求失败: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取响应失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("setUserSettings 失败 (HTTP %d): %s", resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
var result SetUserSettingsResponse
|
||||||
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("响应解析失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchUserInfo 调用 fetchUserInfo API 获取用户隐私设置状态
|
||||||
|
func (c *Client) FetchUserInfo(ctx context.Context, accessToken, projectID string) (*FetchUserInfoResponse, error) {
|
||||||
|
reqBody := FetchUserInfoRequest{Project: projectID}
|
||||||
|
bodyBytes, err := json.Marshal(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("序列化请求失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
apiURL := privacyBaseURL + "/v1internal:fetchUserInfo"
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(bodyBytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "*/*")
|
||||||
|
req.Header.Set("User-Agent", GetUserAgent())
|
||||||
|
req.Header.Set("X-Goog-Api-Client", "gl-node/22.21.1")
|
||||||
|
req.Host = "daily-cloudcode-pa.googleapis.com"
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetchUserInfo 请求失败: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取响应失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("fetchUserInfo 失败 (HTTP %d): %s", resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
var result FetchUserInfoResponse
|
||||||
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("响应解析失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -257,6 +257,7 @@ func registerAccountRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|||||||
accounts.POST("/:id/test", h.Admin.Account.Test)
|
accounts.POST("/:id/test", h.Admin.Account.Test)
|
||||||
accounts.POST("/:id/recover-state", h.Admin.Account.RecoverState)
|
accounts.POST("/:id/recover-state", h.Admin.Account.RecoverState)
|
||||||
accounts.POST("/:id/refresh", h.Admin.Account.Refresh)
|
accounts.POST("/:id/refresh", h.Admin.Account.Refresh)
|
||||||
|
accounts.POST("/:id/set-privacy", h.Admin.Account.SetPrivacy)
|
||||||
accounts.POST("/:id/refresh-tier", h.Admin.Account.RefreshTier)
|
accounts.POST("/:id/refresh-tier", h.Admin.Account.RefreshTier)
|
||||||
accounts.GET("/:id/stats", h.Admin.Account.GetStats)
|
accounts.GET("/:id/stats", h.Admin.Account.GetStats)
|
||||||
accounts.POST("/:id/clear-error", h.Admin.Account.ClearError)
|
accounts.POST("/:id/clear-error", h.Admin.Account.ClearError)
|
||||||
|
|||||||
@ -65,6 +65,10 @@ type AdminService interface {
|
|||||||
SetAccountError(ctx context.Context, id int64, errorMsg string) error
|
SetAccountError(ctx context.Context, id int64, errorMsg string) error
|
||||||
// EnsureOpenAIPrivacy 检查 OpenAI OAuth 账号 privacy_mode,未设置则尝试关闭训练数据共享并持久化。
|
// EnsureOpenAIPrivacy 检查 OpenAI OAuth 账号 privacy_mode,未设置则尝试关闭训练数据共享并持久化。
|
||||||
EnsureOpenAIPrivacy(ctx context.Context, account *Account) string
|
EnsureOpenAIPrivacy(ctx context.Context, account *Account) string
|
||||||
|
// EnsureAntigravityPrivacy 检查 Antigravity OAuth 账号 privacy_mode,未设置则调用 setUserSettings 并持久化。
|
||||||
|
EnsureAntigravityPrivacy(ctx context.Context, account *Account) string
|
||||||
|
// ForceAntigravityPrivacy 强制重新设置 Antigravity OAuth 账号隐私,无论当前状态。
|
||||||
|
ForceAntigravityPrivacy(ctx context.Context, account *Account) string
|
||||||
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
|
CheckMixedChannelRisk(ctx context.Context, currentAccountID int64, currentAccountPlatform string, groupIDs []int64) error
|
||||||
@ -2661,3 +2665,79 @@ func (s *adminServiceImpl) EnsureOpenAIPrivacy(ctx context.Context, account *Acc
|
|||||||
_ = s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{"privacy_mode": mode})
|
_ = s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{"privacy_mode": mode})
|
||||||
return mode
|
return mode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnsureAntigravityPrivacy 检查 Antigravity OAuth 账号隐私状态。
|
||||||
|
// 如果 Extra["privacy_mode"] 已存在(无论成功或失败),直接跳过。
|
||||||
|
// 仅对从未设置过隐私的账号执行 setUserSettings + fetchUserInfo 流程。
|
||||||
|
// 用户可通过前端 ForceAntigravityPrivacy(SetPrivacy 按钮)强制重新设置。
|
||||||
|
func (s *adminServiceImpl) EnsureAntigravityPrivacy(ctx context.Context, account *Account) string {
|
||||||
|
if account.Platform != PlatformAntigravity || account.Type != AccountTypeOAuth {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// 已设置过则跳过(无论成功或失败),用户可通过 Force 手动重试
|
||||||
|
if account.Extra != nil {
|
||||||
|
if existing, ok := account.Extra["privacy_mode"].(string); ok && existing != "" {
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token, _ := account.Credentials["access_token"].(string)
|
||||||
|
if token == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
projectID, _ := account.Credentials["project_id"].(string)
|
||||||
|
|
||||||
|
var proxyURL string
|
||||||
|
if account.ProxyID != nil {
|
||||||
|
if p, err := s.proxyRepo.GetByID(ctx, *account.ProxyID); err == nil && p != nil {
|
||||||
|
proxyURL = p.URL()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := setAntigravityPrivacy(ctx, token, projectID, proxyURL)
|
||||||
|
if mode == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{"privacy_mode": mode}); err != nil {
|
||||||
|
logger.LegacyPrintf("service.admin", "update_antigravity_privacy_mode_failed: account_id=%d err=%v", account.ID, err)
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
applyAntigravityPrivacyMode(account, mode)
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForceAntigravityPrivacy 强制重新设置 Antigravity OAuth 账号隐私,无论当前状态。
|
||||||
|
func (s *adminServiceImpl) ForceAntigravityPrivacy(ctx context.Context, account *Account) string {
|
||||||
|
if account.Platform != PlatformAntigravity || account.Type != AccountTypeOAuth {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
token, _ := account.Credentials["access_token"].(string)
|
||||||
|
if token == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
projectID, _ := account.Credentials["project_id"].(string)
|
||||||
|
|
||||||
|
var proxyURL string
|
||||||
|
if account.ProxyID != nil {
|
||||||
|
if p, err := s.proxyRepo.GetByID(ctx, *account.ProxyID); err == nil && p != nil {
|
||||||
|
proxyURL = p.URL()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := setAntigravityPrivacy(ctx, token, projectID, proxyURL)
|
||||||
|
if mode == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{"privacy_mode": mode}); err != nil {
|
||||||
|
logger.LegacyPrintf("service.admin", "force_update_antigravity_privacy_mode_failed: account_id=%d err=%v", account.ID, err)
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
applyAntigravityPrivacyMode(account, mode)
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
81
backend/internal/service/antigravity_privacy_service.go
Normal file
81
backend/internal/service/antigravity_privacy_service.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AntigravityPrivacySet = "privacy_set"
|
||||||
|
AntigravityPrivacyFailed = "privacy_set_failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setAntigravityPrivacy 调用 Antigravity API 设置隐私并验证结果。
|
||||||
|
// 流程:
|
||||||
|
// 1. setUserSettings 清空设置 → 检查返回值 {"userSettings":{}}
|
||||||
|
// 2. fetchUserInfo 二次验证隐私是否已生效(需要 project_id)
|
||||||
|
//
|
||||||
|
// 返回 privacy_mode 值:"privacy_set" 成功,"privacy_set_failed" 失败,空串表示无法执行。
|
||||||
|
func setAntigravityPrivacy(ctx context.Context, accessToken, projectID, proxyURL string) string {
|
||||||
|
if accessToken == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client, err := antigravity.NewClient(proxyURL)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("antigravity_privacy_client_error", "error", err.Error())
|
||||||
|
return AntigravityPrivacyFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第 1 步:调用 setUserSettings,检查返回值
|
||||||
|
setResp, err := client.SetUserSettings(ctx, accessToken)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("antigravity_privacy_set_failed", "error", err.Error())
|
||||||
|
return AntigravityPrivacyFailed
|
||||||
|
}
|
||||||
|
if !setResp.IsSuccess() {
|
||||||
|
slog.Warn("antigravity_privacy_set_response_not_empty",
|
||||||
|
"user_settings", setResp.UserSettings,
|
||||||
|
)
|
||||||
|
return AntigravityPrivacyFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第 2 步:调用 fetchUserInfo 二次验证隐私是否已生效
|
||||||
|
if strings.TrimSpace(projectID) == "" {
|
||||||
|
slog.Warn("antigravity_privacy_missing_project_id")
|
||||||
|
return AntigravityPrivacyFailed
|
||||||
|
}
|
||||||
|
userInfo, err := client.FetchUserInfo(ctx, accessToken, projectID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("antigravity_privacy_verify_failed", "error", err.Error())
|
||||||
|
return AntigravityPrivacyFailed
|
||||||
|
}
|
||||||
|
if !userInfo.IsPrivate() {
|
||||||
|
slog.Warn("antigravity_privacy_verify_not_private",
|
||||||
|
"user_settings", userInfo.UserSettings,
|
||||||
|
)
|
||||||
|
return AntigravityPrivacyFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("antigravity_privacy_set_success")
|
||||||
|
return AntigravityPrivacySet
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyAntigravityPrivacyMode(account *Account, mode string) {
|
||||||
|
if account == nil || strings.TrimSpace(mode) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
extra := make(map[string]any, len(account.Extra)+1)
|
||||||
|
for k, v := range account.Extra {
|
||||||
|
extra[k] = v
|
||||||
|
}
|
||||||
|
extra["privacy_mode"] = mode
|
||||||
|
account.Extra = extra
|
||||||
|
}
|
||||||
18
backend/internal/service/antigravity_privacy_service_test.go
Normal file
18
backend/internal/service/antigravity_privacy_service_test.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
//go:build unit
|
||||||
|
|
||||||
|
package service
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestApplyAntigravityPrivacyMode_SetsInMemoryExtra(t *testing.T) {
|
||||||
|
account := &Account{}
|
||||||
|
|
||||||
|
applyAntigravityPrivacyMode(account, AntigravityPrivacySet)
|
||||||
|
|
||||||
|
if account.Extra == nil {
|
||||||
|
t.Fatal("expected account.Extra to be initialized")
|
||||||
|
}
|
||||||
|
if got := account.Extra["privacy_mode"]; got != AntigravityPrivacySet {
|
||||||
|
t.Fatalf("expected privacy_mode %q, got %v", AntigravityPrivacySet, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -128,7 +128,7 @@ func (s *TokenRefreshService) Start() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop 停止刷新服务
|
// Stop 停止刷新服务(可安全多次调用)
|
||||||
func (s *TokenRefreshService) Stop() {
|
func (s *TokenRefreshService) Stop() {
|
||||||
close(s.stopCh)
|
close(s.stopCh)
|
||||||
s.wg.Wait()
|
s.wg.Wait()
|
||||||
@ -404,6 +404,8 @@ func (s *TokenRefreshService) postRefreshActions(ctx context.Context, account *A
|
|||||||
}
|
}
|
||||||
// OpenAI OAuth: 刷新成功后,检查是否已设置 privacy_mode,未设置则尝试关闭训练数据共享
|
// OpenAI OAuth: 刷新成功后,检查是否已设置 privacy_mode,未设置则尝试关闭训练数据共享
|
||||||
s.ensureOpenAIPrivacy(ctx, account)
|
s.ensureOpenAIPrivacy(ctx, account)
|
||||||
|
// Antigravity OAuth: 刷新成功后,检查是否已设置 privacy_mode,未设置则调用 setUserSettings
|
||||||
|
s.ensureAntigravityPrivacy(ctx, account)
|
||||||
}
|
}
|
||||||
|
|
||||||
// errRefreshSkipped 表示刷新被跳过(锁竞争或已被其他路径刷新),不计入 failed 或 refreshed
|
// errRefreshSkipped 表示刷新被跳过(锁竞争或已被其他路径刷新),不计入 failed 或 refreshed
|
||||||
@ -477,3 +479,51 @@ func (s *TokenRefreshService) ensureOpenAIPrivacy(ctx context.Context, account *
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensureAntigravityPrivacy 后台刷新中检查 Antigravity OAuth 账号隐私状态。
|
||||||
|
// 仅做 Extra["privacy_mode"] 存在性检查,不发起 HTTP 请求,避免每轮循环产生额外网络开销。
|
||||||
|
// 用户可通过前端 SetPrivacy 按钮强制重新设置。
|
||||||
|
func (s *TokenRefreshService) ensureAntigravityPrivacy(ctx context.Context, account *Account) {
|
||||||
|
if account.Platform != PlatformAntigravity || account.Type != AccountTypeOAuth {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 已设置过(无论成功或失败)则跳过,不发 HTTP
|
||||||
|
if account.Extra != nil {
|
||||||
|
if _, ok := account.Extra["privacy_mode"]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token, _ := account.Credentials["access_token"].(string)
|
||||||
|
if token == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projectID, _ := account.Credentials["project_id"].(string)
|
||||||
|
|
||||||
|
var proxyURL string
|
||||||
|
if account.ProxyID != nil && s.proxyRepo != nil {
|
||||||
|
if p, err := s.proxyRepo.GetByID(ctx, *account.ProxyID); err == nil && p != nil {
|
||||||
|
proxyURL = p.URL()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := setAntigravityPrivacy(ctx, token, projectID, proxyURL)
|
||||||
|
if mode == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{"privacy_mode": mode}); err != nil {
|
||||||
|
slog.Warn("token_refresh.update_antigravity_privacy_mode_failed",
|
||||||
|
"account_id", account.ID,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
applyAntigravityPrivacyMode(account, mode)
|
||||||
|
slog.Info("token_refresh.antigravity_privacy_mode_set",
|
||||||
|
"account_id", account.ID,
|
||||||
|
"privacy_mode", mode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -627,6 +627,16 @@ export async function batchRefresh(accountIds: number[]): Promise<BatchOperation
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set privacy for an Antigravity OAuth account
|
||||||
|
* @param id - Account ID
|
||||||
|
* @returns Updated account
|
||||||
|
*/
|
||||||
|
export async function setPrivacy(id: number): Promise<Account> {
|
||||||
|
const { data } = await apiClient.post<Account>(`/admin/accounts/${id}/set-privacy`)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
export const accountsAPI = {
|
export const accountsAPI = {
|
||||||
list,
|
list,
|
||||||
listWithEtag,
|
listWithEtag,
|
||||||
@ -663,7 +673,8 @@ export const accountsAPI = {
|
|||||||
importData,
|
importData,
|
||||||
getAntigravityDefaultModelMapping,
|
getAntigravityDefaultModelMapping,
|
||||||
batchClearError,
|
batchClearError,
|
||||||
batchRefresh
|
batchRefresh,
|
||||||
|
setPrivacy
|
||||||
}
|
}
|
||||||
|
|
||||||
export default accountsAPI
|
export default accountsAPI
|
||||||
|
|||||||
@ -32,6 +32,10 @@
|
|||||||
{{ t('admin.accounts.refreshToken') }}
|
{{ t('admin.accounts.refreshToken') }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
<button v-if="isAntigravityOAuth" @click="$emit('set-privacy', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm text-emerald-600 hover:bg-gray-100 dark:hover:bg-dark-700">
|
||||||
|
<Icon name="shield" size="sm" />
|
||||||
|
{{ t('admin.accounts.setPrivacy') }}
|
||||||
|
</button>
|
||||||
<div v-if="hasRecoverableState" class="my-1 border-t border-gray-100 dark:border-dark-700"></div>
|
<div v-if="hasRecoverableState" class="my-1 border-t border-gray-100 dark:border-dark-700"></div>
|
||||||
<button v-if="hasRecoverableState" @click="$emit('recover-state', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm text-emerald-600 hover:bg-gray-100 dark:hover:bg-dark-700">
|
<button v-if="hasRecoverableState" @click="$emit('recover-state', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm text-emerald-600 hover:bg-gray-100 dark:hover:bg-dark-700">
|
||||||
<Icon name="sync" size="sm" />
|
<Icon name="sync" size="sm" />
|
||||||
@ -55,7 +59,7 @@ import { Icon } from '@/components/icons'
|
|||||||
import type { Account } from '@/types'
|
import type { Account } from '@/types'
|
||||||
|
|
||||||
const props = defineProps<{ show: boolean; account: Account | null; position: { top: number; left: number } | null }>()
|
const props = defineProps<{ show: boolean; account: Account | null; position: { top: number; left: number } | null }>()
|
||||||
const emit = defineEmits(['close', 'test', 'stats', 'schedule', 'reauth', 'refresh-token', 'recover-state', 'reset-quota'])
|
const emit = defineEmits(['close', 'test', 'stats', 'schedule', 'reauth', 'refresh-token', 'recover-state', 'reset-quota', 'set-privacy'])
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const isRateLimited = computed(() => {
|
const isRateLimited = computed(() => {
|
||||||
if (props.account?.rate_limit_reset_at && new Date(props.account.rate_limit_reset_at) > new Date()) {
|
if (props.account?.rate_limit_reset_at && new Date(props.account.rate_limit_reset_at) > new Date()) {
|
||||||
@ -75,6 +79,7 @@ const isTempUnschedulable = computed(() => props.account?.temp_unschedulable_unt
|
|||||||
const hasRecoverableState = computed(() => {
|
const hasRecoverableState = computed(() => {
|
||||||
return props.account?.status === 'error' || Boolean(isRateLimited.value) || Boolean(isOverloaded.value) || Boolean(isTempUnschedulable.value)
|
return props.account?.status === 'error' || Boolean(isRateLimited.value) || Boolean(isOverloaded.value) || Boolean(isTempUnschedulable.value)
|
||||||
})
|
})
|
||||||
|
const isAntigravityOAuth = computed(() => props.account?.platform === 'antigravity' && props.account?.type === 'oauth')
|
||||||
const hasQuotaLimit = computed(() => {
|
const hasQuotaLimit = computed(() => {
|
||||||
return (props.account?.type === 'apikey' || props.account?.type === 'bedrock') && (
|
return (props.account?.type === 'apikey' || props.account?.type === 'bedrock') && (
|
||||||
(props.account?.quota_limit ?? 0) > 0 ||
|
(props.account?.quota_limit ?? 0) > 0 ||
|
||||||
|
|||||||
@ -139,18 +139,27 @@ const typeClass = computed(() => {
|
|||||||
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
|
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
|
||||||
})
|
})
|
||||||
|
|
||||||
// Privacy badge — shows different states for OpenAI OAuth training setting
|
// Privacy badge — shows different states for OpenAI/Antigravity OAuth privacy setting
|
||||||
const privacyBadge = computed(() => {
|
const privacyBadge = computed(() => {
|
||||||
if (props.platform !== 'openai' || props.type !== 'oauth' || !props.privacyMode) return null
|
if (props.type !== 'oauth' || !props.privacyMode) return null
|
||||||
|
// 支持 OpenAI 和 Antigravity 平台
|
||||||
|
if (props.platform !== 'openai' && props.platform !== 'antigravity') return null
|
||||||
|
|
||||||
const shieldCheck = 'M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z'
|
const shieldCheck = 'M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z'
|
||||||
const shieldX = 'M12 9v3.75m0-10.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285zM12 18h.008v.008H12V18z'
|
const shieldX = 'M12 9v3.75m0-10.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285zM12 18h.008v.008H12V18z'
|
||||||
switch (props.privacyMode) {
|
switch (props.privacyMode) {
|
||||||
|
// OpenAI states
|
||||||
case 'training_off':
|
case 'training_off':
|
||||||
return { label: 'Privacy', icon: shieldCheck, title: t('admin.accounts.privacyTrainingOff'), class: 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400' }
|
return { label: 'Private', icon: shieldCheck, title: t('admin.accounts.privacyTrainingOff'), class: 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400' }
|
||||||
case 'training_set_cf_blocked':
|
case 'training_set_cf_blocked':
|
||||||
return { label: 'CF', icon: shieldX, title: t('admin.accounts.privacyCfBlocked'), class: 'bg-yellow-100 text-yellow-600 dark:bg-yellow-900/30 dark:text-yellow-400' }
|
return { label: 'CF', icon: shieldX, title: t('admin.accounts.privacyCfBlocked'), class: 'bg-yellow-100 text-yellow-600 dark:bg-yellow-900/30 dark:text-yellow-400' }
|
||||||
case 'training_set_failed':
|
case 'training_set_failed':
|
||||||
return { label: 'Fail', icon: shieldX, title: t('admin.accounts.privacyFailed'), class: 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400' }
|
return { label: 'Fail', icon: shieldX, title: t('admin.accounts.privacyFailed'), class: 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400' }
|
||||||
|
// Antigravity states
|
||||||
|
case 'privacy_set':
|
||||||
|
return { label: 'Private', icon: shieldCheck, title: t('admin.accounts.privacyAntigravitySet'), class: 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400' }
|
||||||
|
case 'privacy_set_failed':
|
||||||
|
return { label: 'Fail', icon: shieldX, title: t('admin.accounts.privacyAntigravityFailed'), class: 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400' }
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1984,6 +1984,9 @@ export default {
|
|||||||
privacyTrainingOff: 'Training data sharing disabled',
|
privacyTrainingOff: 'Training data sharing disabled',
|
||||||
privacyCfBlocked: 'Blocked by Cloudflare, training may still be on',
|
privacyCfBlocked: 'Blocked by Cloudflare, training may still be on',
|
||||||
privacyFailed: 'Failed to disable training',
|
privacyFailed: 'Failed to disable training',
|
||||||
|
privacyAntigravitySet: 'Telemetry and marketing emails disabled',
|
||||||
|
privacyAntigravityFailed: 'Privacy setting failed',
|
||||||
|
setPrivacy: 'Set Privacy',
|
||||||
// Capacity status tooltips
|
// Capacity status tooltips
|
||||||
capacity: {
|
capacity: {
|
||||||
windowCost: {
|
windowCost: {
|
||||||
|
|||||||
@ -2022,6 +2022,9 @@ export default {
|
|||||||
privacyTrainingOff: '已关闭训练数据共享',
|
privacyTrainingOff: '已关闭训练数据共享',
|
||||||
privacyCfBlocked: '被 Cloudflare 拦截,训练可能仍开启',
|
privacyCfBlocked: '被 Cloudflare 拦截,训练可能仍开启',
|
||||||
privacyFailed: '关闭训练数据共享失败',
|
privacyFailed: '关闭训练数据共享失败',
|
||||||
|
privacyAntigravitySet: '已关闭遥测和营销邮件',
|
||||||
|
privacyAntigravityFailed: '隐私设置失败',
|
||||||
|
setPrivacy: '设置隐私',
|
||||||
// 容量状态提示
|
// 容量状态提示
|
||||||
capacity: {
|
capacity: {
|
||||||
windowCost: {
|
windowCost: {
|
||||||
|
|||||||
@ -276,7 +276,7 @@
|
|||||||
<AccountTestModal :show="showTest" :account="testingAcc" @close="closeTestModal" />
|
<AccountTestModal :show="showTest" :account="testingAcc" @close="closeTestModal" />
|
||||||
<AccountStatsModal :show="showStats" :account="statsAcc" @close="closeStatsModal" />
|
<AccountStatsModal :show="showStats" :account="statsAcc" @close="closeStatsModal" />
|
||||||
<ScheduledTestsPanel :show="showSchedulePanel" :account-id="scheduleAcc?.id ?? null" :model-options="scheduleModelOptions" @close="closeSchedulePanel" />
|
<ScheduledTestsPanel :show="showSchedulePanel" :account-id="scheduleAcc?.id ?? null" :model-options="scheduleModelOptions" @close="closeSchedulePanel" />
|
||||||
<AccountActionMenu :show="menu.show" :account="menu.acc" :position="menu.pos" @close="menu.show = false" @test="handleTest" @stats="handleViewStats" @schedule="handleSchedule" @reauth="handleReAuth" @refresh-token="handleRefresh" @recover-state="handleRecoverState" @reset-quota="handleResetQuota" />
|
<AccountActionMenu :show="menu.show" :account="menu.acc" :position="menu.pos" @close="menu.show = false" @test="handleTest" @stats="handleViewStats" @schedule="handleSchedule" @reauth="handleReAuth" @refresh-token="handleRefresh" @recover-state="handleRecoverState" @reset-quota="handleResetQuota" @set-privacy="handleSetPrivacy" />
|
||||||
<SyncFromCrsModal :show="showSync" @close="showSync = false" @synced="reload" />
|
<SyncFromCrsModal :show="showSync" @close="showSync = false" @synced="reload" />
|
||||||
<ImportDataModal :show="showImportData" @close="showImportData = false" @imported="handleDataImported" />
|
<ImportDataModal :show="showImportData" @close="showImportData = false" @imported="handleDataImported" />
|
||||||
<BulkEditAccountModal :show="showBulkEdit" :account-ids="selIds" :selected-platforms="selPlatforms" :selected-types="selTypes" :proxies="proxies" :groups="groups" @close="showBulkEdit = false" @updated="handleBulkUpdated" />
|
<BulkEditAccountModal :show="showBulkEdit" :account-ids="selIds" :selected-platforms="selPlatforms" :selected-types="selTypes" :proxies="proxies" :groups="groups" @close="showBulkEdit = false" @updated="handleBulkUpdated" />
|
||||||
@ -1241,6 +1241,17 @@ const handleResetQuota = async (a: Account) => {
|
|||||||
console.error('Failed to reset quota:', error)
|
console.error('Failed to reset quota:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const handleSetPrivacy = async (a: Account) => {
|
||||||
|
try {
|
||||||
|
const updated = await adminAPI.accounts.setPrivacy(a.id)
|
||||||
|
patchAccountInList(updated)
|
||||||
|
enterAutoRefreshSilentWindow()
|
||||||
|
appStore.showSuccess(t('common.success'))
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to set privacy:', error)
|
||||||
|
appStore.showError(error?.response?.data?.message || t('admin.accounts.privacyAntigravityFailed'))
|
||||||
|
}
|
||||||
|
}
|
||||||
const handleDelete = (a: Account) => { deletingAcc.value = a; showDeleteDialog.value = true }
|
const handleDelete = (a: Account) => { deletingAcc.value = a; showDeleteDialog.value = true }
|
||||||
const confirmDelete = async () => { if(!deletingAcc.value) return; try { await adminAPI.accounts.delete(deletingAcc.value.id); showDeleteDialog.value = false; deletingAcc.value = null; reload() } catch (error) { console.error('Failed to delete account:', error) } }
|
const confirmDelete = async () => { if(!deletingAcc.value) return; try { await adminAPI.accounts.delete(deletingAcc.value.id); showDeleteDialog.value = false; deletingAcc.value = null; reload() } catch (error) { console.error('Failed to delete account:', error) } }
|
||||||
const handleToggleSchedulable = async (a: Account) => {
|
const handleToggleSchedulable = async (a: Account) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user