Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
a63de121
Commit
a63de121
authored
Mar 12, 2026
by
QTom
Browse files
feat: GPT 隐私模式 + no-train 前端展示优化
parent
826090e0
Changes
15
Hide whitespace changes
Inline
Side-by-side
backend/cmd/server/wire.go
View file @
a63de121
...
@@ -41,6 +41,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -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) {
...
@@ -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
,
...
...
backend/cmd/server/wire_gen.go
View file @
a63de121
...
@@ -104,7 +104,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -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) {
...
@@ -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 {
...
@@ -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
,
...
...
backend/internal/handler/admin/account_handler.go
View file @
a63de121
...
@@ -865,6 +865,9 @@ func (h *AccountHandler) refreshSingleAccount(ctx context.Context, account *serv
...
@@ -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
}
}
...
...
backend/internal/handler/admin/admin_service_stub_test.go
View file @
a63de121
...
@@ -429,5 +429,9 @@ func (s *stubAdminService) ResetAccountQuota(ctx context.Context, id int64) erro
...
@@ -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
)
backend/internal/handler/admin/openai_oauth_handler.go
View file @
a63de121
...
@@ -289,6 +289,7 @@ func (h *OpenAIOAuthHandler) CreateAccountFromOAuth(c *gin.Context) {
...
@@ -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
,
...
...
backend/internal/repository/req_client_pool.go
View file @
a63de121
...
@@ -73,3 +73,14 @@ func buildReqClientKey(opts reqClientOptions) string {
...
@@ -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
})
}
backend/internal/server/api_contract_test.go
View file @
a63de121
...
@@ -645,7 +645,7 @@ func newContractDeps(t *testing.T) *contractDeps {
...
@@ -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
)
...
...
backend/internal/service/admin_service.go
View file @
a63de121
...
@@ -57,6 +57,8 @@ type AdminService interface {
...
@@ -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 {
...
@@ -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(
...
@@ -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(
...
@@ -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
...
@@ -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
.
E
xtra
,
Extra
:
e
xtra
,
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 {
...
@@ -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
}
backend/internal/service/openai_privacy_service.go
0 → 100644
View file @
a63de121
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
)
}
backend/internal/service/token_refresh_service.go
View file @
a63de121
...
@@ -21,6 +21,10 @@ type TokenRefreshService struct {
...
@@ -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) {
...
@@ -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
...
@@ -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 {
...
@@ -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
,
)
}
}
backend/internal/service/wire.go
View file @
a63de121
...
@@ -49,10 +49,14 @@ func ProvideTokenRefreshService(
...
@@ -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
}
}
...
...
frontend/src/components/common/PlatformTypeBadge.vue
View file @
a63de121
<
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(() => {
...
@@ -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
>
frontend/src/i18n/locales/en.ts
View file @
a63de121
...
@@ -1743,6 +1743,9 @@ export default {
...
@@ -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
:
{
...
...
frontend/src/i18n/locales/zh.ts
View file @
a63de121
...
@@ -1792,6 +1792,9 @@ export default {
...
@@ -1792,6 +1792,9 @@ export default {
expiresAt
:
'
过期时间
'
,
expiresAt
:
'
过期时间
'
,
actions
:
'
操作
'
actions
:
'
操作
'
},
},
privacyTrainingOff
:
'
已关闭训练数据共享
'
,
privacyCfBlocked
:
'
被 Cloudflare 拦截,训练可能仍开启
'
,
privacyFailed
:
'
关闭训练数据共享失败
'
,
// 容量状态提示
// 容量状态提示
capacity
:
{
capacity
:
{
windowCost
:
{
windowCost
:
{
...
...
frontend/src/views/admin/AccountsView.vue
View file @
a63de121
...
@@ -171,7 +171,7 @@
...
@@ -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
"
/>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment