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
9398ea7a
Unverified
Commit
9398ea7a
authored
Mar 27, 2026
by
Wesley Liddick
Committed by
GitHub
Mar 27, 2026
Browse files
Merge pull request #1340 from DaydreamCoding/fix/privacy-and-system-prompt
fix(openai): OpenAI 隐私模式全场景覆盖 & 修复转发路径 system prompt 丢失
parents
29dce1a5
c729ee42
Changes
8
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/account_handler.go
View file @
9398ea7a
...
...
@@ -539,6 +539,8 @@ func (h *AccountHandler) Create(c *gin.Context) {
}
// Antigravity OAuth: 新账号直接设置隐私
h
.
adminService
.
ForceAntigravityPrivacy
(
ctx
,
account
)
// OpenAI OAuth: 新账号直接设置隐私
h
.
adminService
.
ForceOpenAIPrivacy
(
ctx
,
account
)
return
h
.
buildAccountResponseWithRuntime
(
ctx
,
account
),
nil
})
if
err
!=
nil
{
...
...
@@ -785,6 +787,8 @@ func (h *AccountHandler) refreshSingleAccount(ctx context.Context, account *serv
if
account
.
IsOpenAI
()
{
tokenInfo
,
err
:=
h
.
openaiOAuthService
.
RefreshAccountToken
(
ctx
,
account
)
if
err
!=
nil
{
// 刷新失败但 access_token 可能仍有效,尝试设置隐私
h
.
adminService
.
EnsureOpenAIPrivacy
(
ctx
,
account
)
return
nil
,
""
,
err
}
...
...
@@ -1159,8 +1163,9 @@ func (h *AccountHandler) BatchCreate(c *gin.Context) {
success
:=
0
failed
:=
0
results
:=
make
([]
gin
.
H
,
0
,
len
(
req
.
Accounts
))
// 收集需要异步设置隐私的 Antigravity OAuth 账号
var
privacyAccounts
[]
*
service
.
Account
// 收集需要异步设置隐私的 OAuth 账号
var
antigravityPrivacyAccounts
[]
*
service
.
Account
var
openaiPrivacyAccounts
[]
*
service
.
Account
for
_
,
item
:=
range
req
.
Accounts
{
if
item
.
RateMultiplier
!=
nil
&&
*
item
.
RateMultiplier
<
0
{
...
...
@@ -1203,9 +1208,14 @@ func (h *AccountHandler) BatchCreate(c *gin.Context) {
})
continue
}
// 收集 Antigravity OAuth 账号,稍后异步设置隐私
if
account
.
Platform
==
service
.
PlatformAntigravity
&&
account
.
Type
==
service
.
AccountTypeOAuth
{
privacyAccounts
=
append
(
privacyAccounts
,
account
)
// 收集需要异步设置隐私的 OAuth 账号
if
account
.
Type
==
service
.
AccountTypeOAuth
{
switch
account
.
Platform
{
case
service
.
PlatformAntigravity
:
antigravityPrivacyAccounts
=
append
(
antigravityPrivacyAccounts
,
account
)
case
service
.
PlatformOpenAI
:
openaiPrivacyAccounts
=
append
(
openaiPrivacyAccounts
,
account
)
}
}
success
++
results
=
append
(
results
,
gin
.
H
{
...
...
@@ -1215,9 +1225,10 @@ func (h *AccountHandler) BatchCreate(c *gin.Context) {
})
}
// 异步设置 Antigravity 隐私,避免批量创建时阻塞请求
if
len
(
privacyAccounts
)
>
0
{
adminSvc
:=
h
.
adminService
// 异步设置隐私,避免批量创建时阻塞请求
adminSvc
:=
h
.
adminService
if
len
(
antigravityPrivacyAccounts
)
>
0
{
accounts
:=
antigravityPrivacyAccounts
go
func
()
{
defer
func
()
{
if
r
:=
recover
();
r
!=
nil
{
...
...
@@ -1225,11 +1236,25 @@ func (h *AccountHandler) BatchCreate(c *gin.Context) {
}
}()
bgCtx
:=
context
.
Background
()
for
_
,
acc
:=
range
privacyA
ccounts
{
for
_
,
acc
:=
range
a
ccounts
{
adminSvc
.
ForceAntigravityPrivacy
(
bgCtx
,
acc
)
}
}()
}
if
len
(
openaiPrivacyAccounts
)
>
0
{
accounts
:=
openaiPrivacyAccounts
go
func
()
{
defer
func
()
{
if
r
:=
recover
();
r
!=
nil
{
slog
.
Error
(
"batch_create_openai_privacy_panic"
,
"recover"
,
r
)
}
}()
bgCtx
:=
context
.
Background
()
for
_
,
acc
:=
range
accounts
{
adminSvc
.
ForceOpenAIPrivacy
(
bgCtx
,
acc
)
}
}()
}
return
gin
.
H
{
"success"
:
success
,
...
...
@@ -1896,7 +1921,7 @@ func (h *AccountHandler) GetAvailableModels(c *gin.Context) {
response
.
Success
(
c
,
models
)
}
// SetPrivacy handles setting privacy for a single Antigravity OAuth account
// SetPrivacy handles setting privacy for a single
OpenAI/
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
)
...
...
@@ -1909,11 +1934,20 @@ func (h *AccountHandler) SetPrivacy(c *gin.Context) {
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"
)
if
account
.
Type
!=
service
.
AccountTypeOAuth
{
response
.
BadRequest
(
c
,
"Only OAuth accounts support privacy setting"
)
return
}
var
mode
string
switch
account
.
Platform
{
case
service
.
PlatformOpenAI
:
mode
=
h
.
adminService
.
ForceOpenAIPrivacy
(
c
.
Request
.
Context
(),
account
)
case
service
.
PlatformAntigravity
:
mode
=
h
.
adminService
.
ForceAntigravityPrivacy
(
c
.
Request
.
Context
(),
account
)
default
:
response
.
BadRequest
(
c
,
"Only OpenAI and 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
...
...
backend/internal/handler/admin/admin_service_stub_test.go
View file @
9398ea7a
...
...
@@ -449,6 +449,10 @@ func (s *stubAdminService) EnsureAntigravityPrivacy(ctx context.Context, account
return
""
}
func
(
s
*
stubAdminService
)
ForceOpenAIPrivacy
(
ctx
context
.
Context
,
account
*
service
.
Account
)
string
{
return
""
}
func
(
s
*
stubAdminService
)
ForceAntigravityPrivacy
(
ctx
context
.
Context
,
account
*
service
.
Account
)
string
{
return
""
}
...
...
backend/internal/service/admin_service.go
View file @
9398ea7a
...
...
@@ -67,6 +67,8 @@ type AdminService interface {
EnsureOpenAIPrivacy
(
ctx
context
.
Context
,
account
*
Account
)
string
// EnsureAntigravityPrivacy 检查 Antigravity OAuth 账号 privacy_mode,未设置则调用 setUserSettings 并持久化。
EnsureAntigravityPrivacy
(
ctx
context
.
Context
,
account
*
Account
)
string
// ForceOpenAIPrivacy 强制重新设置 OpenAI OAuth 账号隐私,无论当前状态。
ForceOpenAIPrivacy
(
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
)
...
...
@@ -2664,6 +2666,43 @@ func (s *adminServiceImpl) EnsureOpenAIPrivacy(ctx context.Context, account *Acc
return
mode
}
// ForceOpenAIPrivacy 强制重新设置 OpenAI OAuth 账号隐私,无论当前状态。
func
(
s
*
adminServiceImpl
)
ForceOpenAIPrivacy
(
ctx
context
.
Context
,
account
*
Account
)
string
{
if
account
.
Platform
!=
PlatformOpenAI
||
account
.
Type
!=
AccountTypeOAuth
{
return
""
}
if
s
.
privacyClientFactory
==
nil
{
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
""
}
if
err
:=
s
.
accountRepo
.
UpdateExtra
(
ctx
,
account
.
ID
,
map
[
string
]
any
{
"privacy_mode"
:
mode
});
err
!=
nil
{
logger
.
LegacyPrintf
(
"service.admin"
,
"force_update_openai_privacy_mode_failed: account_id=%d err=%v"
,
account
.
ID
,
err
)
return
mode
}
if
account
.
Extra
==
nil
{
account
.
Extra
=
make
(
map
[
string
]
any
)
}
account
.
Extra
[
"privacy_mode"
]
=
mode
return
mode
}
// EnsureAntigravityPrivacy 检查 Antigravity OAuth 账号隐私状态。
// 如果 Extra["privacy_mode"] 已存在(无论成功或失败),直接跳过。
// 仅对从未设置过隐私的账号执行 setUserSettings + fetchUserInfo 流程。
...
...
backend/internal/service/gateway_prompt_test.go
View file @
9398ea7a
...
...
@@ -124,6 +124,27 @@ func TestSystemIncludesClaudeCodePrompt(t *testing.T) {
},
want
:
false
,
},
// json.RawMessage cases (conversion path: ForwardAsResponses / ForwardAsChatCompletions)
{
name
:
"json.RawMessage string with Claude Code prompt"
,
system
:
json
.
RawMessage
(
`"`
+
claudeCodeSystemPrompt
+
`"`
),
want
:
true
,
},
{
name
:
"json.RawMessage string without Claude Code prompt"
,
system
:
json
.
RawMessage
(
`"You are a helpful assistant"`
),
want
:
false
,
},
{
name
:
"json.RawMessage nil (empty)"
,
system
:
json
.
RawMessage
(
nil
),
want
:
false
,
},
{
name
:
"json.RawMessage empty string"
,
system
:
json
.
RawMessage
(
`""`
),
want
:
false
,
},
}
for
_
,
tt
:=
range
tests
{
...
...
@@ -202,6 +223,29 @@ func TestInjectClaudeCodePrompt(t *testing.T) {
wantSystemLen
:
1
,
wantFirstText
:
claudeCodeSystemPrompt
,
},
// json.RawMessage cases (conversion path: ForwardAsResponses / ForwardAsChatCompletions)
{
name
:
"json.RawMessage string system"
,
body
:
`{"model":"claude-3","system":"Custom prompt"}`
,
system
:
json
.
RawMessage
(
`"Custom prompt"`
),
wantSystemLen
:
2
,
wantFirstText
:
claudeCodeSystemPrompt
,
wantSecondText
:
claudePrefix
+
"
\n\n
Custom prompt"
,
},
{
name
:
"json.RawMessage nil system"
,
body
:
`{"model":"claude-3"}`
,
system
:
json
.
RawMessage
(
nil
),
wantSystemLen
:
1
,
wantFirstText
:
claudeCodeSystemPrompt
,
},
{
name
:
"json.RawMessage Claude Code prompt (should not duplicate)"
,
body
:
`{"model":"claude-3","system":"`
+
claudeCodeSystemPrompt
+
`"}`
,
system
:
json
.
RawMessage
(
`"`
+
claudeCodeSystemPrompt
+
`"`
),
wantSystemLen
:
1
,
wantFirstText
:
claudeCodeSystemPrompt
,
},
}
for
_
,
tt
:=
range
tests
{
...
...
backend/internal/service/gateway_service.go
View file @
9398ea7a
...
...
@@ -3749,9 +3749,28 @@ func isClaudeCodeRequest(ctx context.Context, c *gin.Context, parsed *ParsedRequ
return
isClaudeCodeClient
(
c
.
GetHeader
(
"User-Agent"
),
parsed
.
MetadataUserID
)
}
// normalizeSystemParam 将 json.RawMessage 类型的 system 参数转为标准 Go 类型(string / []any / nil),
// 避免 type switch 中 json.RawMessage(底层 []byte)无法匹配 case string / case []any / case nil 的问题。
// 这是 Go 的 typed nil 陷阱:(json.RawMessage, nil) ≠ (nil, nil)。
func
normalizeSystemParam
(
system
any
)
any
{
raw
,
ok
:=
system
.
(
json
.
RawMessage
)
if
!
ok
{
return
system
}
if
len
(
raw
)
==
0
{
return
nil
}
var
parsed
any
if
err
:=
json
.
Unmarshal
(
raw
,
&
parsed
);
err
!=
nil
{
return
nil
}
return
parsed
}
// systemIncludesClaudeCodePrompt 检查 system 中是否已包含 Claude Code 提示词
// 使用前缀匹配支持多种变体(标准版、Agent SDK 版等)
func
systemIncludesClaudeCodePrompt
(
system
any
)
bool
{
system
=
normalizeSystemParam
(
system
)
switch
v
:=
system
.
(
type
)
{
case
string
:
return
hasClaudeCodePrefix
(
v
)
...
...
@@ -3780,6 +3799,7 @@ func hasClaudeCodePrefix(text string) bool {
// injectClaudeCodePrompt 在 system 开头注入 Claude Code 提示词
// 处理 null、字符串、数组三种格式
func
injectClaudeCodePrompt
(
body
[]
byte
,
system
any
)
[]
byte
{
system
=
normalizeSystemParam
(
system
)
claudeCodeBlock
,
err
:=
marshalAnthropicSystemTextBlock
(
claudeCodeSystemPrompt
,
true
)
if
err
!=
nil
{
logger
.
LegacyPrintf
(
"service.gateway"
,
"Warning: failed to build Claude Code prompt block: %v"
,
err
)
...
...
backend/internal/service/token_refresh_service.go
View file @
9398ea7a
...
...
@@ -300,6 +300,8 @@ func (s *TokenRefreshService) refreshWithRetry(ctx context.Context, account *Acc
"error"
,
setErr
,
)
}
// 刷新失败但 access_token 可能仍有效,尝试设置隐私
s
.
ensureOpenAIPrivacy
(
ctx
,
account
)
return
err
}
...
...
@@ -327,6 +329,9 @@ func (s *TokenRefreshService) refreshWithRetry(ctx context.Context, account *Acc
"error"
,
lastErr
,
)
// 刷新失败但 access_token 可能仍有效,尝试设置隐私
s
.
ensureOpenAIPrivacy
(
ctx
,
account
)
// 设置临时不可调度 10 分钟(不标记 error,保持 status=active 让下个刷新周期能继续尝试)
until
:=
time
.
Now
()
.
Add
(
tokenRefreshTempUnschedDuration
)
reason
:=
fmt
.
Sprintf
(
"token refresh retry exhausted: %v"
,
lastErr
)
...
...
frontend/src/components/admin/account/AccountActionMenu.vue
View file @
9398ea7a
...
...
@@ -32,7 +32,7 @@
{{
t
(
'
admin.accounts.refreshToken
'
)
}}
</button>
</
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"
>
<button
v-if=
"
supportsPrivacy
"
@
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>
...
...
@@ -80,6 +80,8 @@ const hasRecoverableState = computed(() => {
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
isOpenAIOAuth
=
computed
(()
=>
props
.
account
?.
platform
===
'
openai
'
&&
props
.
account
?.
type
===
'
oauth
'
)
const
supportsPrivacy
=
computed
(()
=>
isAntigravityOAuth
.
value
||
isOpenAIOAuth
.
value
)
const
hasQuotaLimit
=
computed
(()
=>
{
return
(
props
.
account
?.
type
===
'
apikey
'
||
props
.
account
?.
type
===
'
bedrock
'
)
&&
(
(
props
.
account
?.
quota_limit
??
0
)
>
0
||
...
...
frontend/src/views/admin/AccountsView.vue
View file @
9398ea7a
...
...
@@ -1262,7 +1262,7 @@ const handleSetPrivacy = async (a: Account) => {
appStore
.
showSuccess
(
t
(
'
common.success
'
))
}
catch
(
error
:
any
)
{
console
.
error
(
'
Failed to set privacy:
'
,
error
)
appStore
.
showError
(
error
?.
response
?.
data
?.
message
||
t
(
'
admin.accounts.privacy
Antigravity
Failed
'
))
appStore
.
showError
(
error
?.
response
?.
data
?.
message
||
t
(
'
admin.accounts.privacyFailed
'
))
}
}
const
handleDelete
=
(
a
:
Account
)
=>
{
deletingAcc
.
value
=
a
;
showDeleteDialog
.
value
=
true
}
...
...
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