Skip to content

Commit c2965c0

Browse files
author
QTom
committed
feat(antigravity): 自动设置隐私并支持后台手动重试
新增 Antigravity OAuth 隐私设置能力,在账号创建、刷新、导入和后台 Token 刷新路径自动调用 setUserSettings + fetchUserInfo 关闭遥测; 持久化后同步内存 Extra,错误处理改为日志记录。 Made-with: Cursor
1 parent 0f03393 commit c2965c0

15 files changed

Lines changed: 512 additions & 8 deletions

File tree

backend/internal/handler/admin/account_data.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,9 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest)
267267
}
268268
}
269269

270+
// 收集需要异步设置隐私的 Antigravity OAuth 账号
271+
var privacyAccounts []*service.Account
272+
270273
for i := range dataPayload.Accounts {
271274
item := dataPayload.Accounts[i]
272275
if err := validateDataAccount(item); err != nil {
@@ -314,7 +317,8 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest)
314317
SkipDefaultGroupBind: skipDefaultGroupBind,
315318
}
316319

317-
if _, err := h.adminService.CreateAccount(ctx, accountInput); err != nil {
320+
created, err := h.adminService.CreateAccount(ctx, accountInput)
321+
if err != nil {
318322
result.AccountFailed++
319323
result.Errors = append(result.Errors, DataImportError{
320324
Kind: "account",
@@ -323,9 +327,30 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest)
323327
})
324328
continue
325329
}
330+
// 收集 Antigravity OAuth 账号,稍后异步设置隐私
331+
if created.Platform == service.PlatformAntigravity && created.Type == service.AccountTypeOAuth {
332+
privacyAccounts = append(privacyAccounts, created)
333+
}
326334
result.AccountCreated++
327335
}
328336

337+
// 异步设置 Antigravity 隐私,避免大量导入时阻塞请求
338+
if len(privacyAccounts) > 0 {
339+
adminSvc := h.adminService
340+
go func() {
341+
defer func() {
342+
if r := recover(); r != nil {
343+
slog.Error("import_antigravity_privacy_panic", "recover", r)
344+
}
345+
}()
346+
bgCtx := context.Background()
347+
for _, acc := range privacyAccounts {
348+
adminSvc.ForceAntigravityPrivacy(bgCtx, acc)
349+
}
350+
slog.Info("import_antigravity_privacy_done", "count", len(privacyAccounts))
351+
}()
352+
}
353+
329354
return result, nil
330355
}
331356

backend/internal/handler/admin/account_handler.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"errors"
1010
"fmt"
1111
"log"
12+
"log/slog"
1213
"net/http"
1314
"strconv"
1415
"strings"
@@ -536,6 +537,8 @@ func (h *AccountHandler) Create(c *gin.Context) {
536537
if execErr != nil {
537538
return nil, execErr
538539
}
540+
// Antigravity OAuth: 新账号直接设置隐私
541+
h.adminService.ForceAntigravityPrivacy(ctx, account)
539542
return h.buildAccountResponseWithRuntime(ctx, account), nil
540543
})
541544
if err != nil {
@@ -883,6 +886,8 @@ func (h *AccountHandler) refreshSingleAccount(ctx context.Context, account *serv
883886

884887
// OpenAI OAuth: 刷新成功后检查并设置 privacy_mode
885888
h.adminService.EnsureOpenAIPrivacy(ctx, updatedAccount)
889+
// Antigravity OAuth: 刷新成功后检查并设置 privacy_mode
890+
h.adminService.EnsureAntigravityPrivacy(ctx, updatedAccount)
886891

887892
return updatedAccount, "", nil
888893
}
@@ -1154,6 +1159,8 @@ func (h *AccountHandler) BatchCreate(c *gin.Context) {
11541159
success := 0
11551160
failed := 0
11561161
results := make([]gin.H, 0, len(req.Accounts))
1162+
// 收集需要异步设置隐私的 Antigravity OAuth 账号
1163+
var privacyAccounts []*service.Account
11571164

11581165
for _, item := range req.Accounts {
11591166
if item.RateMultiplier != nil && *item.RateMultiplier < 0 {
@@ -1196,6 +1203,10 @@ func (h *AccountHandler) BatchCreate(c *gin.Context) {
11961203
})
11971204
continue
11981205
}
1206+
// 收集 Antigravity OAuth 账号,稍后异步设置隐私
1207+
if account.Platform == service.PlatformAntigravity && account.Type == service.AccountTypeOAuth {
1208+
privacyAccounts = append(privacyAccounts, account)
1209+
}
11991210
success++
12001211
results = append(results, gin.H{
12011212
"name": item.Name,
@@ -1204,6 +1215,22 @@ func (h *AccountHandler) BatchCreate(c *gin.Context) {
12041215
})
12051216
}
12061217

1218+
// 异步设置 Antigravity 隐私,避免批量创建时阻塞请求
1219+
if len(privacyAccounts) > 0 {
1220+
adminSvc := h.adminService
1221+
go func() {
1222+
defer func() {
1223+
if r := recover(); r != nil {
1224+
slog.Error("batch_create_antigravity_privacy_panic", "recover", r)
1225+
}
1226+
}()
1227+
bgCtx := context.Background()
1228+
for _, acc := range privacyAccounts {
1229+
adminSvc.ForceAntigravityPrivacy(bgCtx, acc)
1230+
}
1231+
}()
1232+
}
1233+
12071234
return gin.H{
12081235
"success": success,
12091236
"failed": failed,
@@ -1869,6 +1896,42 @@ func (h *AccountHandler) GetAvailableModels(c *gin.Context) {
18691896
response.Success(c, models)
18701897
}
18711898

1899+
// SetPrivacy handles setting privacy for a single Antigravity OAuth account
1900+
// POST /api/v1/admin/accounts/:id/set-privacy
1901+
func (h *AccountHandler) SetPrivacy(c *gin.Context) {
1902+
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
1903+
if err != nil {
1904+
response.BadRequest(c, "Invalid account ID")
1905+
return
1906+
}
1907+
account, err := h.adminService.GetAccount(c.Request.Context(), accountID)
1908+
if err != nil {
1909+
response.NotFound(c, "Account not found")
1910+
return
1911+
}
1912+
if account.Platform != service.PlatformAntigravity || account.Type != service.AccountTypeOAuth {
1913+
response.BadRequest(c, "Only Antigravity OAuth accounts support privacy setting")
1914+
return
1915+
}
1916+
mode := h.adminService.ForceAntigravityPrivacy(c.Request.Context(), account)
1917+
if mode == "" {
1918+
response.BadRequest(c, "Cannot set privacy: missing access_token")
1919+
return
1920+
}
1921+
// 从 DB 重新读取以确保返回最新状态
1922+
updated, err := h.adminService.GetAccount(c.Request.Context(), accountID)
1923+
if err != nil {
1924+
// 隐私已设置成功但读取失败,回退到内存更新
1925+
if account.Extra == nil {
1926+
account.Extra = make(map[string]any)
1927+
}
1928+
account.Extra["privacy_mode"] = mode
1929+
response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
1930+
return
1931+
}
1932+
response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), updated))
1933+
}
1934+
18721935
// RefreshTier handles refreshing Google One tier for a single account
18731936
// POST /api/v1/admin/accounts/:id/refresh-tier
18741937
func (h *AccountHandler) RefreshTier(c *gin.Context) {

backend/internal/handler/admin/admin_service_stub_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,14 @@ func (s *stubAdminService) EnsureOpenAIPrivacy(ctx context.Context, account *ser
445445
return ""
446446
}
447447

448+
func (s *stubAdminService) EnsureAntigravityPrivacy(ctx context.Context, account *service.Account) string {
449+
return ""
450+
}
451+
452+
func (s *stubAdminService) ForceAntigravityPrivacy(ctx context.Context, account *service.Account) string {
453+
return ""
454+
}
455+
448456
func (s *stubAdminService) ReplaceUserGroup(ctx context.Context, userID, oldGroupID, newGroupID int64) (*service.ReplaceUserGroupResult, error) {
449457
return &service.ReplaceUserGroupResult{MigratedKeys: 0}, nil
450458
}

backend/internal/pkg/antigravity/client.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,3 +704,139 @@ func (c *Client) FetchAvailableModels(ctx context.Context, accessToken, projectI
704704

705705
return nil, nil, lastErr
706706
}
707+
708+
// ── Privacy API ──────────────────────────────────────────────────────
709+
710+
// privacyBaseURL 隐私设置 API 仅使用 daily 端点(与 Antigravity 客户端行为一致)
711+
const privacyBaseURL = antigravityDailyBaseURL
712+
713+
// SetUserSettingsRequest setUserSettings 请求体
714+
type SetUserSettingsRequest struct {
715+
UserSettings map[string]any `json:"user_settings"`
716+
}
717+
718+
// FetchUserInfoRequest fetchUserInfo 请求体
719+
type FetchUserInfoRequest struct {
720+
Project string `json:"project"`
721+
}
722+
723+
// FetchUserInfoResponse fetchUserInfo 响应体
724+
type FetchUserInfoResponse struct {
725+
UserSettings map[string]any `json:"userSettings,omitempty"`
726+
RegionCode string `json:"regionCode,omitempty"`
727+
}
728+
729+
// IsPrivate 判断隐私是否已设置:userSettings 为空或不含 telemetryEnabled 表示已设置
730+
func (r *FetchUserInfoResponse) IsPrivate() bool {
731+
if r == nil || r.UserSettings == nil {
732+
return true
733+
}
734+
_, hasTelemetry := r.UserSettings["telemetryEnabled"]
735+
return !hasTelemetry
736+
}
737+
738+
// SetUserSettingsResponse setUserSettings 响应体
739+
type SetUserSettingsResponse struct {
740+
UserSettings map[string]any `json:"userSettings,omitempty"`
741+
}
742+
743+
// IsSuccess 判断 setUserSettings 是否成功:返回 {"userSettings":{}} 且无 telemetryEnabled
744+
func (r *SetUserSettingsResponse) IsSuccess() bool {
745+
if r == nil {
746+
return false
747+
}
748+
// userSettings 为 nil 或空 map 均视为成功
749+
if r.UserSettings == nil || len(r.UserSettings) == 0 {
750+
return true
751+
}
752+
// 如果包含 telemetryEnabled 字段,说明未成功清除
753+
_, hasTelemetry := r.UserSettings["telemetryEnabled"]
754+
return !hasTelemetry
755+
}
756+
757+
// SetUserSettings 调用 setUserSettings API 设置用户隐私,返回解析后的响应
758+
func (c *Client) SetUserSettings(ctx context.Context, accessToken string) (*SetUserSettingsResponse, error) {
759+
// 发送空 user_settings 以清除隐私设置
760+
payload := SetUserSettingsRequest{UserSettings: map[string]any{}}
761+
bodyBytes, err := json.Marshal(payload)
762+
if err != nil {
763+
return nil, fmt.Errorf("序列化请求失败: %w", err)
764+
}
765+
766+
apiURL := privacyBaseURL + "/v1internal:setUserSettings"
767+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(bodyBytes))
768+
if err != nil {
769+
return nil, fmt.Errorf("创建请求失败: %w", err)
770+
}
771+
req.Header.Set("Authorization", "Bearer "+accessToken)
772+
req.Header.Set("Content-Type", "application/json")
773+
req.Header.Set("Accept", "*/*")
774+
req.Header.Set("User-Agent", GetUserAgent())
775+
req.Header.Set("X-Goog-Api-Client", "gl-node/22.21.1")
776+
req.Host = "daily-cloudcode-pa.googleapis.com"
777+
778+
resp, err := c.httpClient.Do(req)
779+
if err != nil {
780+
return nil, fmt.Errorf("setUserSettings 请求失败: %w", err)
781+
}
782+
defer func() { _ = resp.Body.Close() }()
783+
784+
respBody, err := io.ReadAll(resp.Body)
785+
if err != nil {
786+
return nil, fmt.Errorf("读取响应失败: %w", err)
787+
}
788+
789+
if resp.StatusCode != http.StatusOK {
790+
return nil, fmt.Errorf("setUserSettings 失败 (HTTP %d): %s", resp.StatusCode, string(respBody))
791+
}
792+
793+
var result SetUserSettingsResponse
794+
if err := json.Unmarshal(respBody, &result); err != nil {
795+
return nil, fmt.Errorf("响应解析失败: %w", err)
796+
}
797+
798+
return &result, nil
799+
}
800+
801+
// FetchUserInfo 调用 fetchUserInfo API 获取用户隐私设置状态
802+
func (c *Client) FetchUserInfo(ctx context.Context, accessToken, projectID string) (*FetchUserInfoResponse, error) {
803+
reqBody := FetchUserInfoRequest{Project: projectID}
804+
bodyBytes, err := json.Marshal(reqBody)
805+
if err != nil {
806+
return nil, fmt.Errorf("序列化请求失败: %w", err)
807+
}
808+
809+
apiURL := privacyBaseURL + "/v1internal:fetchUserInfo"
810+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(bodyBytes))
811+
if err != nil {
812+
return nil, fmt.Errorf("创建请求失败: %w", err)
813+
}
814+
req.Header.Set("Authorization", "Bearer "+accessToken)
815+
req.Header.Set("Content-Type", "application/json")
816+
req.Header.Set("Accept", "*/*")
817+
req.Header.Set("User-Agent", GetUserAgent())
818+
req.Header.Set("X-Goog-Api-Client", "gl-node/22.21.1")
819+
req.Host = "daily-cloudcode-pa.googleapis.com"
820+
821+
resp, err := c.httpClient.Do(req)
822+
if err != nil {
823+
return nil, fmt.Errorf("fetchUserInfo 请求失败: %w", err)
824+
}
825+
defer func() { _ = resp.Body.Close() }()
826+
827+
respBody, err := io.ReadAll(resp.Body)
828+
if err != nil {
829+
return nil, fmt.Errorf("读取响应失败: %w", err)
830+
}
831+
832+
if resp.StatusCode != http.StatusOK {
833+
return nil, fmt.Errorf("fetchUserInfo 失败 (HTTP %d): %s", resp.StatusCode, string(respBody))
834+
}
835+
836+
var result FetchUserInfoResponse
837+
if err := json.Unmarshal(respBody, &result); err != nil {
838+
return nil, fmt.Errorf("响应解析失败: %w", err)
839+
}
840+
841+
return &result, nil
842+
}

backend/internal/server/routes/admin.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ func registerAccountRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
257257
accounts.POST("/:id/test", h.Admin.Account.Test)
258258
accounts.POST("/:id/recover-state", h.Admin.Account.RecoverState)
259259
accounts.POST("/:id/refresh", h.Admin.Account.Refresh)
260+
accounts.POST("/:id/set-privacy", h.Admin.Account.SetPrivacy)
260261
accounts.POST("/:id/refresh-tier", h.Admin.Account.RefreshTier)
261262
accounts.GET("/:id/stats", h.Admin.Account.GetStats)
262263
accounts.POST("/:id/clear-error", h.Admin.Account.ClearError)

0 commit comments

Comments
 (0)