mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-03-30 04:44:49 +00:00
feat: GPT 隐私模式 + no-train 前端展示优化
This commit is contained in:
@@ -41,6 +41,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
// Server layer ProviderSet
|
// Server layer ProviderSet
|
||||||
server.ProviderSet,
|
server.ProviderSet,
|
||||||
|
|
||||||
|
// Privacy client factory for OpenAI training opt-out
|
||||||
|
providePrivacyClientFactory,
|
||||||
|
|
||||||
// BuildInfo provider
|
// BuildInfo provider
|
||||||
provideServiceBuildInfo,
|
provideServiceBuildInfo,
|
||||||
|
|
||||||
@@ -53,6 +56,10 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func providePrivacyClientFactory() service.PrivacyClientFactory {
|
||||||
|
return repository.CreatePrivacyReqClient
|
||||||
|
}
|
||||||
|
|
||||||
func provideServiceBuildInfo(buildInfo handler.BuildInfo) service.BuildInfo {
|
func provideServiceBuildInfo(buildInfo handler.BuildInfo) service.BuildInfo {
|
||||||
return service.BuildInfo{
|
return service.BuildInfo{
|
||||||
Version: buildInfo.Version,
|
Version: buildInfo.Version,
|
||||||
|
|||||||
@@ -104,7 +104,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
proxyRepository := repository.NewProxyRepository(client, db)
|
proxyRepository := repository.NewProxyRepository(client, db)
|
||||||
proxyExitInfoProber := repository.NewProxyExitInfoProber(configConfig)
|
proxyExitInfoProber := repository.NewProxyExitInfoProber(configConfig)
|
||||||
proxyLatencyCache := repository.NewProxyLatencyCache(redisClient)
|
proxyLatencyCache := repository.NewProxyLatencyCache(redisClient)
|
||||||
adminService := service.NewAdminService(userRepository, groupRepository, accountRepository, soraAccountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, userGroupRateRepository, billingCacheService, proxyExitInfoProber, proxyLatencyCache, apiKeyAuthCacheInvalidator, client, settingService, subscriptionService, userSubscriptionRepository)
|
privacyClientFactory := providePrivacyClientFactory()
|
||||||
|
adminService := service.NewAdminService(userRepository, groupRepository, accountRepository, soraAccountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, userGroupRateRepository, billingCacheService, proxyExitInfoProber, proxyLatencyCache, apiKeyAuthCacheInvalidator, client, settingService, subscriptionService, userSubscriptionRepository, privacyClientFactory)
|
||||||
concurrencyCache := repository.ProvideConcurrencyCache(redisClient, configConfig)
|
concurrencyCache := repository.ProvideConcurrencyCache(redisClient, configConfig)
|
||||||
concurrencyService := service.ProvideConcurrencyService(concurrencyCache, accountRepository, configConfig)
|
concurrencyService := service.ProvideConcurrencyService(concurrencyCache, accountRepository, configConfig)
|
||||||
adminUserHandler := admin.NewUserHandler(adminService, concurrencyService)
|
adminUserHandler := admin.NewUserHandler(adminService, concurrencyService)
|
||||||
@@ -226,7 +227,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
opsCleanupService := service.ProvideOpsCleanupService(opsRepository, db, redisClient, configConfig)
|
opsCleanupService := service.ProvideOpsCleanupService(opsRepository, db, redisClient, configConfig)
|
||||||
opsScheduledReportService := service.ProvideOpsScheduledReportService(opsService, userService, emailService, redisClient, configConfig)
|
opsScheduledReportService := service.ProvideOpsScheduledReportService(opsService, userService, emailService, redisClient, configConfig)
|
||||||
soraMediaCleanupService := service.ProvideSoraMediaCleanupService(soraMediaStorage, configConfig)
|
soraMediaCleanupService := service.ProvideSoraMediaCleanupService(soraMediaStorage, configConfig)
|
||||||
tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, soraAccountRepository, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, compositeTokenCacheInvalidator, schedulerCache, configConfig, tempUnschedCache)
|
tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, soraAccountRepository, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, compositeTokenCacheInvalidator, schedulerCache, configConfig, tempUnschedCache, privacyClientFactory, proxyRepository)
|
||||||
accountExpiryService := service.ProvideAccountExpiryService(accountRepository)
|
accountExpiryService := service.ProvideAccountExpiryService(accountRepository)
|
||||||
subscriptionExpiryService := service.ProvideSubscriptionExpiryService(userSubscriptionRepository)
|
subscriptionExpiryService := service.ProvideSubscriptionExpiryService(userSubscriptionRepository)
|
||||||
scheduledTestRunnerService := service.ProvideScheduledTestRunnerService(scheduledTestPlanRepository, scheduledTestService, accountTestService, rateLimitService, configConfig)
|
scheduledTestRunnerService := service.ProvideScheduledTestRunnerService(scheduledTestPlanRepository, scheduledTestService, accountTestService, rateLimitService, configConfig)
|
||||||
@@ -245,6 +246,10 @@ type Application struct {
|
|||||||
Cleanup func()
|
Cleanup func()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func providePrivacyClientFactory() service.PrivacyClientFactory {
|
||||||
|
return repository.CreatePrivacyReqClient
|
||||||
|
}
|
||||||
|
|
||||||
func provideServiceBuildInfo(buildInfo handler.BuildInfo) service.BuildInfo {
|
func provideServiceBuildInfo(buildInfo handler.BuildInfo) service.BuildInfo {
|
||||||
return service.BuildInfo{
|
return service.BuildInfo{
|
||||||
Version: buildInfo.Version,
|
Version: buildInfo.Version,
|
||||||
|
|||||||
@@ -865,6 +865,9 @@ func (h *AccountHandler) refreshSingleAccount(ctx context.Context, account *serv
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenAI OAuth: 刷新成功后检查并设置 privacy_mode
|
||||||
|
h.adminService.EnsureOpenAIPrivacy(ctx, updatedAccount)
|
||||||
|
|
||||||
return updatedAccount, "", nil
|
return updatedAccount, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -429,5 +429,9 @@ func (s *stubAdminService) ResetAccountQuota(ctx context.Context, id int64) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *stubAdminService) EnsureOpenAIPrivacy(ctx context.Context, account *service.Account) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure stub implements interface.
|
// Ensure stub implements interface.
|
||||||
var _ service.AdminService = (*stubAdminService)(nil)
|
var _ service.AdminService = (*stubAdminService)(nil)
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ func (h *OpenAIOAuthHandler) CreateAccountFromOAuth(c *gin.Context) {
|
|||||||
Platform: platform,
|
Platform: platform,
|
||||||
Type: "oauth",
|
Type: "oauth",
|
||||||
Credentials: credentials,
|
Credentials: credentials,
|
||||||
|
Extra: nil,
|
||||||
ProxyID: req.ProxyID,
|
ProxyID: req.ProxyID,
|
||||||
Concurrency: req.Concurrency,
|
Concurrency: req.Concurrency,
|
||||||
Priority: req.Priority,
|
Priority: req.Priority,
|
||||||
|
|||||||
@@ -73,3 +73,14 @@ func buildReqClientKey(opts reqClientOptions) string {
|
|||||||
opts.ForceHTTP2,
|
opts.ForceHTTP2,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreatePrivacyReqClient creates an HTTP client for OpenAI privacy settings API
|
||||||
|
// This is exported for use by OpenAIPrivacyService
|
||||||
|
// Uses Chrome TLS fingerprint impersonation to bypass Cloudflare checks
|
||||||
|
func CreatePrivacyReqClient(proxyURL string) (*req.Client, error) {
|
||||||
|
return getSharedReqClient(reqClientOptions{
|
||||||
|
ProxyURL: proxyURL,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
Impersonate: true, // Enable Chrome TLS fingerprint impersonation
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -645,7 +645,7 @@ func newContractDeps(t *testing.T) *contractDeps {
|
|||||||
settingRepo := newStubSettingRepo()
|
settingRepo := newStubSettingRepo()
|
||||||
settingService := service.NewSettingService(settingRepo, cfg)
|
settingService := service.NewSettingService(settingRepo, cfg)
|
||||||
|
|
||||||
adminService := service.NewAdminService(userRepo, groupRepo, &accountRepo, nil, proxyRepo, apiKeyRepo, redeemRepo, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
adminService := service.NewAdminService(userRepo, groupRepo, &accountRepo, nil, proxyRepo, apiKeyRepo, redeemRepo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||||
authHandler := handler.NewAuthHandler(cfg, nil, userService, settingService, nil, redeemService, nil)
|
authHandler := handler.NewAuthHandler(cfg, nil, userService, settingService, nil, redeemService, nil)
|
||||||
apiKeyHandler := handler.NewAPIKeyHandler(apiKeyService)
|
apiKeyHandler := handler.NewAPIKeyHandler(apiKeyService)
|
||||||
usageHandler := handler.NewUsageHandler(usageService, apiKeyService)
|
usageHandler := handler.NewUsageHandler(usageService, apiKeyService)
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ type AdminService interface {
|
|||||||
RefreshAccountCredentials(ctx context.Context, id int64) (*Account, error)
|
RefreshAccountCredentials(ctx context.Context, id int64) (*Account, error)
|
||||||
ClearAccountError(ctx context.Context, id int64) (*Account, error)
|
ClearAccountError(ctx context.Context, id int64) (*Account, error)
|
||||||
SetAccountError(ctx context.Context, id int64, errorMsg string) error
|
SetAccountError(ctx context.Context, id int64, errorMsg string) error
|
||||||
|
// EnsureOpenAIPrivacy 检查 OpenAI OAuth 账号 privacy_mode,未设置则尝试关闭训练数据共享并持久化。
|
||||||
|
EnsureOpenAIPrivacy(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
|
||||||
@@ -433,6 +435,7 @@ type adminServiceImpl struct {
|
|||||||
settingService *SettingService
|
settingService *SettingService
|
||||||
defaultSubAssigner DefaultSubscriptionAssigner
|
defaultSubAssigner DefaultSubscriptionAssigner
|
||||||
userSubRepo UserSubscriptionRepository
|
userSubRepo UserSubscriptionRepository
|
||||||
|
privacyClientFactory PrivacyClientFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
type userGroupRateBatchReader interface {
|
type userGroupRateBatchReader interface {
|
||||||
@@ -461,6 +464,7 @@ func NewAdminService(
|
|||||||
settingService *SettingService,
|
settingService *SettingService,
|
||||||
defaultSubAssigner DefaultSubscriptionAssigner,
|
defaultSubAssigner DefaultSubscriptionAssigner,
|
||||||
userSubRepo UserSubscriptionRepository,
|
userSubRepo UserSubscriptionRepository,
|
||||||
|
privacyClientFactory PrivacyClientFactory,
|
||||||
) AdminService {
|
) AdminService {
|
||||||
return &adminServiceImpl{
|
return &adminServiceImpl{
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
@@ -479,6 +483,7 @@ func NewAdminService(
|
|||||||
settingService: settingService,
|
settingService: settingService,
|
||||||
defaultSubAssigner: defaultSubAssigner,
|
defaultSubAssigner: defaultSubAssigner,
|
||||||
userSubRepo: userSubRepo,
|
userSubRepo: userSubRepo,
|
||||||
|
privacyClientFactory: privacyClientFactory,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1420,13 +1425,30 @@ func (s *adminServiceImpl) CreateAccount(ctx context.Context, input *CreateAccou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenAI OAuth: attempt to disable training data sharing
|
||||||
|
extra := input.Extra
|
||||||
|
if input.Platform == PlatformOpenAI && input.Type == AccountTypeOAuth {
|
||||||
|
if token, _ := input.Credentials["access_token"].(string); token != "" {
|
||||||
|
var proxyURL string
|
||||||
|
if input.ProxyID != nil {
|
||||||
|
if p, err := s.proxyRepo.GetByID(ctx, *input.ProxyID); err == nil && p != nil {
|
||||||
|
proxyURL = p.URL()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if extra == nil {
|
||||||
|
extra = make(map[string]any)
|
||||||
|
}
|
||||||
|
extra["privacy_mode"] = disableOpenAITraining(ctx, s.privacyClientFactory, token, proxyURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
account := &Account{
|
account := &Account{
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
Notes: normalizeAccountNotes(input.Notes),
|
Notes: normalizeAccountNotes(input.Notes),
|
||||||
Platform: input.Platform,
|
Platform: input.Platform,
|
||||||
Type: input.Type,
|
Type: input.Type,
|
||||||
Credentials: input.Credentials,
|
Credentials: input.Credentials,
|
||||||
Extra: input.Extra,
|
Extra: extra,
|
||||||
ProxyID: input.ProxyID,
|
ProxyID: input.ProxyID,
|
||||||
Concurrency: input.Concurrency,
|
Concurrency: input.Concurrency,
|
||||||
Priority: input.Priority,
|
Priority: input.Priority,
|
||||||
@@ -2502,3 +2524,39 @@ func (e *MixedChannelError) Error() string {
|
|||||||
func (s *adminServiceImpl) ResetAccountQuota(ctx context.Context, id int64) error {
|
func (s *adminServiceImpl) ResetAccountQuota(ctx context.Context, id int64) error {
|
||||||
return s.accountRepo.ResetQuotaUsed(ctx, id)
|
return s.accountRepo.ResetQuotaUsed(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnsureOpenAIPrivacy 检查 OpenAI OAuth 账号是否已设置 privacy_mode,
|
||||||
|
// 未设置则调用 disableOpenAITraining 并持久化到 Extra,返回设置的 mode 值。
|
||||||
|
func (s *adminServiceImpl) EnsureOpenAIPrivacy(ctx context.Context, account *Account) string {
|
||||||
|
if account.Platform != PlatformOpenAI || account.Type != AccountTypeOAuth {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if s.privacyClientFactory == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if account.Extra != nil {
|
||||||
|
if _, ok := account.Extra["privacy_mode"]; ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token, _ := account.Credentials["access_token"].(string)
|
||||||
|
if token == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var proxyURL string
|
||||||
|
if account.ProxyID != nil {
|
||||||
|
if p, err := s.proxyRepo.GetByID(ctx, *account.ProxyID); err == nil && p != nil {
|
||||||
|
proxyURL = p.URL()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := disableOpenAITraining(ctx, s.privacyClientFactory, token, proxyURL)
|
||||||
|
if mode == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{"privacy_mode": mode})
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
|||||||
77
backend/internal/service/openai_privacy_service.go
Normal file
77
backend/internal/service/openai_privacy_service.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/imroc/req/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrivacyClientFactory creates an HTTP client for privacy API calls.
|
||||||
|
// Injected from repository layer to avoid import cycles.
|
||||||
|
type PrivacyClientFactory func(proxyURL string) (*req.Client, error)
|
||||||
|
|
||||||
|
const (
|
||||||
|
openAISettingsURL = "https://chatgpt.com/backend-api/settings/account_user_setting"
|
||||||
|
|
||||||
|
PrivacyModeTrainingOff = "training_off"
|
||||||
|
PrivacyModeFailed = "training_set_failed"
|
||||||
|
PrivacyModeCFBlocked = "training_set_cf_blocked"
|
||||||
|
)
|
||||||
|
|
||||||
|
// disableOpenAITraining calls ChatGPT settings API to turn off "Improve the model for everyone".
|
||||||
|
// Returns privacy_mode value: "training_off" on success, "cf_blocked" / "failed" on failure.
|
||||||
|
func disableOpenAITraining(ctx context.Context, clientFactory PrivacyClientFactory, accessToken, proxyURL string) string {
|
||||||
|
if accessToken == "" || clientFactory == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client, err := clientFactory(proxyURL)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("openai_privacy_client_error", "error", err.Error())
|
||||||
|
return PrivacyModeFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.R().
|
||||||
|
SetContext(ctx).
|
||||||
|
SetHeader("Authorization", "Bearer "+accessToken).
|
||||||
|
SetHeader("Origin", "https://chatgpt.com").
|
||||||
|
SetHeader("Referer", "https://chatgpt.com/").
|
||||||
|
SetQueryParam("feature", "training_allowed").
|
||||||
|
SetQueryParam("value", "false").
|
||||||
|
Patch(openAISettingsURL)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("openai_privacy_request_error", "error", err.Error())
|
||||||
|
return PrivacyModeFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == 403 || resp.StatusCode == 503 {
|
||||||
|
body := resp.String()
|
||||||
|
if strings.Contains(body, "cloudflare") || strings.Contains(body, "cf-") || strings.Contains(body, "Just a moment") {
|
||||||
|
slog.Warn("openai_privacy_cf_blocked", "status", resp.StatusCode)
|
||||||
|
return PrivacyModeCFBlocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.IsSuccessState() {
|
||||||
|
slog.Warn("openai_privacy_failed", "status", resp.StatusCode, "body", truncate(resp.String(), 200))
|
||||||
|
return PrivacyModeFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("openai_privacy_training_disabled")
|
||||||
|
return PrivacyModeTrainingOff
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncate(s string, n int) string {
|
||||||
|
if len(s) <= n {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s[:n] + fmt.Sprintf("...(%d more)", len(s)-n)
|
||||||
|
}
|
||||||
@@ -21,6 +21,10 @@ type TokenRefreshService struct {
|
|||||||
schedulerCache SchedulerCache // 用于同步更新调度器缓存,解决 token 刷新后缓存不一致问题
|
schedulerCache SchedulerCache // 用于同步更新调度器缓存,解决 token 刷新后缓存不一致问题
|
||||||
tempUnschedCache TempUnschedCache // 用于清除 Redis 中的临时不可调度缓存
|
tempUnschedCache TempUnschedCache // 用于清除 Redis 中的临时不可调度缓存
|
||||||
|
|
||||||
|
// OpenAI privacy: 刷新成功后检查并设置 training opt-out
|
||||||
|
privacyClientFactory PrivacyClientFactory
|
||||||
|
proxyRepo ProxyRepository
|
||||||
|
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
@@ -72,6 +76,12 @@ func (s *TokenRefreshService) SetSoraAccountRepo(repo SoraAccountRepository) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetPrivacyDeps 注入 OpenAI privacy opt-out 所需依赖
|
||||||
|
func (s *TokenRefreshService) SetPrivacyDeps(factory PrivacyClientFactory, proxyRepo ProxyRepository) {
|
||||||
|
s.privacyClientFactory = factory
|
||||||
|
s.proxyRepo = proxyRepo
|
||||||
|
}
|
||||||
|
|
||||||
// Start 启动后台刷新服务
|
// Start 启动后台刷新服务
|
||||||
func (s *TokenRefreshService) Start() {
|
func (s *TokenRefreshService) Start() {
|
||||||
if !s.cfg.Enabled {
|
if !s.cfg.Enabled {
|
||||||
@@ -277,6 +287,8 @@ func (s *TokenRefreshService) refreshWithRetry(ctx context.Context, account *Acc
|
|||||||
slog.Debug("token_refresh.scheduler_cache_synced", "account_id", account.ID)
|
slog.Debug("token_refresh.scheduler_cache_synced", "account_id", account.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// OpenAI OAuth: 刷新成功后,检查是否已设置 privacy_mode,未设置则尝试关闭训练数据共享
|
||||||
|
s.ensureOpenAIPrivacy(ctx, account)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,3 +353,49 @@ func isNonRetryableRefreshError(err error) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensureOpenAIPrivacy 检查 OpenAI OAuth 账号是否已设置 privacy_mode,
|
||||||
|
// 未设置则调用 disableOpenAITraining 并持久化结果到 Extra。
|
||||||
|
func (s *TokenRefreshService) ensureOpenAIPrivacy(ctx context.Context, account *Account) {
|
||||||
|
if account.Platform != PlatformOpenAI || account.Type != AccountTypeOAuth {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.privacyClientFactory == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 已设置过则跳过
|
||||||
|
if account.Extra != nil {
|
||||||
|
if _, ok := account.Extra["privacy_mode"]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token, _ := account.Credentials["access_token"].(string)
|
||||||
|
if token == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := disableOpenAITraining(ctx, s.privacyClientFactory, token, 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_privacy_mode_failed",
|
||||||
|
"account_id", account.ID,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
slog.Info("token_refresh.privacy_mode_set",
|
||||||
|
"account_id", account.ID,
|
||||||
|
"privacy_mode", mode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,10 +49,14 @@ func ProvideTokenRefreshService(
|
|||||||
schedulerCache SchedulerCache,
|
schedulerCache SchedulerCache,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
tempUnschedCache TempUnschedCache,
|
tempUnschedCache TempUnschedCache,
|
||||||
|
privacyClientFactory PrivacyClientFactory,
|
||||||
|
proxyRepo ProxyRepository,
|
||||||
) *TokenRefreshService {
|
) *TokenRefreshService {
|
||||||
svc := NewTokenRefreshService(accountRepo, oauthService, openaiOAuthService, geminiOAuthService, antigravityOAuthService, cacheInvalidator, schedulerCache, cfg, tempUnschedCache)
|
svc := NewTokenRefreshService(accountRepo, oauthService, openaiOAuthService, geminiOAuthService, antigravityOAuthService, cacheInvalidator, schedulerCache, cfg, tempUnschedCache)
|
||||||
// 注入 Sora 账号扩展表仓储,用于 OpenAI Token 刷新时同步 sora_accounts 表
|
// 注入 Sora 账号扩展表仓储,用于 OpenAI Token 刷新时同步 sora_accounts 表
|
||||||
svc.SetSoraAccountRepo(soraAccountRepo)
|
svc.SetSoraAccountRepo(soraAccountRepo)
|
||||||
|
// 注入 OpenAI privacy opt-out 依赖
|
||||||
|
svc.SetPrivacyDeps(privacyClientFactory, proxyRepo)
|
||||||
svc.Start()
|
svc.Start()
|
||||||
return svc
|
return svc
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,67 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="inline-flex items-center overflow-hidden rounded-md text-xs font-medium">
|
<div class="inline-flex flex-col gap-0.5 text-xs font-medium">
|
||||||
<!-- Platform part -->
|
<!-- Row 1: Platform + Type -->
|
||||||
<span :class="['inline-flex items-center gap-1 px-2 py-1', platformClass]">
|
<div class="inline-flex items-center overflow-hidden rounded-md">
|
||||||
<PlatformIcon :platform="platform" size="xs" />
|
<span :class="['inline-flex items-center gap-1 px-2 py-1', platformClass]">
|
||||||
<span>{{ platformLabel }}</span>
|
<PlatformIcon :platform="platform" size="xs" />
|
||||||
</span>
|
<span>{{ platformLabel }}</span>
|
||||||
<!-- Type part -->
|
</span>
|
||||||
<span :class="['inline-flex items-center gap-1 px-1.5 py-1', typeClass]">
|
<span :class="['inline-flex items-center gap-1 px-1.5 py-1', typeClass]">
|
||||||
<!-- OAuth icon -->
|
<!-- OAuth icon -->
|
||||||
<svg
|
<svg
|
||||||
v-if="type === 'oauth'"
|
v-if="type === 'oauth'"
|
||||||
class="h-3 w-3"
|
class="h-3 w-3"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Setup Token icon -->
|
||||||
|
<Icon v-else-if="type === 'setup-token'" name="shield" size="xs" />
|
||||||
|
<!-- API Key icon -->
|
||||||
|
<Icon v-else name="key" size="xs" />
|
||||||
|
<span>{{ typeLabel }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- Row 2: Plan type + Privacy mode (only if either exists) -->
|
||||||
|
<div v-if="planLabel || privacyBadge" class="inline-flex items-center overflow-hidden rounded-md">
|
||||||
|
<span v-if="planLabel" :class="['inline-flex items-center gap-1 px-1.5 py-1', typeClass]">
|
||||||
|
<span>{{ planLabel }}</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="privacyBadge"
|
||||||
|
:class="['inline-flex items-center gap-1 px-1.5 py-1', privacyBadge.class]"
|
||||||
|
:title="privacyBadge.title"
|
||||||
>
|
>
|
||||||
<path
|
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
stroke-linecap="round"
|
<path stroke-linecap="round" stroke-linejoin="round" :d="privacyBadge.icon" />
|
||||||
stroke-linejoin="round"
|
</svg>
|
||||||
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
|
<span>{{ privacyBadge.label }}</span>
|
||||||
/>
|
</span>
|
||||||
</svg>
|
</div>
|
||||||
<!-- Setup Token icon -->
|
|
||||||
<Icon v-else-if="type === 'setup-token'" name="shield" size="xs" />
|
|
||||||
<!-- API Key icon -->
|
|
||||||
<Icon v-else name="key" size="xs" />
|
|
||||||
<span>{{ typeLabel }}</span>
|
|
||||||
</span>
|
|
||||||
<!-- Plan type part (optional) -->
|
|
||||||
<span v-if="planLabel" :class="['inline-flex items-center gap-1 px-1.5 py-1 border-l border-white/20', typeClass]">
|
|
||||||
<span>{{ planLabel }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { AccountPlatform, AccountType } from '@/types'
|
import type { AccountPlatform, AccountType } from '@/types'
|
||||||
import PlatformIcon from './PlatformIcon.vue'
|
import PlatformIcon from './PlatformIcon.vue'
|
||||||
import Icon from '@/components/icons/Icon.vue'
|
import Icon from '@/components/icons/Icon.vue'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
platform: AccountPlatform
|
platform: AccountPlatform
|
||||||
type: AccountType
|
type: AccountType
|
||||||
planType?: string
|
planType?: string
|
||||||
|
privacyMode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
@@ -119,4 +136,21 @@ 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
|
||||||
|
const privacyBadge = computed(() => {
|
||||||
|
if (props.platform !== 'openai' || props.type !== 'oauth' || !props.privacyMode) 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 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) {
|
||||||
|
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' }
|
||||||
|
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' }
|
||||||
|
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' }
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1743,6 +1743,9 @@ export default {
|
|||||||
expiresAt: 'Expires At',
|
expiresAt: 'Expires At',
|
||||||
actions: 'Actions'
|
actions: 'Actions'
|
||||||
},
|
},
|
||||||
|
privacyTrainingOff: 'Training data sharing disabled',
|
||||||
|
privacyCfBlocked: 'Blocked by Cloudflare, training may still be on',
|
||||||
|
privacyFailed: 'Failed to disable training',
|
||||||
// Capacity status tooltips
|
// Capacity status tooltips
|
||||||
capacity: {
|
capacity: {
|
||||||
windowCost: {
|
windowCost: {
|
||||||
|
|||||||
@@ -1792,6 +1792,9 @@ export default {
|
|||||||
expiresAt: '过期时间',
|
expiresAt: '过期时间',
|
||||||
actions: '操作'
|
actions: '操作'
|
||||||
},
|
},
|
||||||
|
privacyTrainingOff: '已关闭训练数据共享',
|
||||||
|
privacyCfBlocked: '被 Cloudflare 拦截,训练可能仍开启',
|
||||||
|
privacyFailed: '关闭训练数据共享失败',
|
||||||
// 容量状态提示
|
// 容量状态提示
|
||||||
capacity: {
|
capacity: {
|
||||||
windowCost: {
|
windowCost: {
|
||||||
|
|||||||
@@ -171,7 +171,7 @@
|
|||||||
<span v-else class="text-sm text-gray-400 dark:text-dark-500">-</span>
|
<span v-else class="text-sm text-gray-400 dark:text-dark-500">-</span>
|
||||||
</template>
|
</template>
|
||||||
<template #cell-platform_type="{ row }">
|
<template #cell-platform_type="{ row }">
|
||||||
<PlatformTypeBadge :platform="row.platform" :type="row.type" :plan-type="row.credentials?.plan_type" />
|
<PlatformTypeBadge :platform="row.platform" :type="row.type" :plan-type="row.credentials?.plan_type" :privacy-mode="row.extra?.privacy_mode" />
|
||||||
</template>
|
</template>
|
||||||
<template #cell-capacity="{ row }">
|
<template #cell-capacity="{ row }">
|
||||||
<AccountCapacityCell :account="row" />
|
<AccountCapacityCell :account="row" />
|
||||||
|
|||||||
Reference in New Issue
Block a user