@@ -132,7 +132,7 @@ const (
132132)
133133
134134// WebsocketExecuteFunc WebSocket 执行函数(由 wsrelay 包在 main.go 中注册,避免循环依赖)
135- var WebsocketExecuteFunc func (ctx context.Context , account * auth.Account , requestBody []byte , sessionID string , proxyOverride string ) (* http.Response , error )
135+ var WebsocketExecuteFunc func (ctx context.Context , account * auth.Account , requestBody []byte , sessionID string , proxyOverride string , apiKey string , deviceCfg * DeviceProfileConfig , headers http. Header ) (* http.Response , error )
136136
137137// ExecuteRequest 向 Codex 上游发送请求
138138// sessionID 可选,用于 prompt cache 会话绑定
@@ -141,7 +141,7 @@ var WebsocketExecuteFunc func(ctx context.Context, account *auth.Account, reques
141141func ExecuteRequest (ctx context.Context , account * auth.Account , requestBody []byte , sessionID string , proxyOverride string , apiKey string , deviceCfg * DeviceProfileConfig , headers http.Header , useWebsocket ... bool ) (* http.Response , error ) {
142142 // 检查是否使用 WebSocket
143143 if len (useWebsocket ) > 0 && useWebsocket [0 ] && WebsocketExecuteFunc != nil {
144- return WebsocketExecuteFunc (ctx , account , requestBody , sessionID , proxyOverride )
144+ return WebsocketExecuteFunc (ctx , account , requestBody , sessionID , proxyOverride , apiKey , deviceCfg , headers )
145145 }
146146
147147 if ctx == nil {
@@ -150,7 +150,6 @@ func ExecuteRequest(ctx context.Context, account *auth.Account, requestBody []by
150150
151151 account .Mu ().RLock ()
152152 accessToken := account .AccessToken
153- accountID := account .AccountID
154153 proxyURL := account .ProxyURL
155154 account .Mu ().RUnlock ()
156155
@@ -193,67 +192,103 @@ func ExecuteRequest(ctx context.Context, account *auth.Account, requestBody []by
193192 }
194193
195194 // ==================== 请求头(伪装 Codex CLI) ====================
196- // 应用设备指纹稳定化
195+ applyCodexRequestHeaders (req , account , accessToken , cacheKey , apiKey , deviceCfg , headers )
196+
197+ // 获取连接池 HTTP 客户端(账号级隔离,复用 TCP/TLS 连接)
198+ client := getPooledClient (account , proxyURL )
199+
200+ resp , err := client .Do (req )
201+ if err != nil {
202+ if shouldRecyclePooledClient (err ) {
203+ recyclePooledClient (account , proxyURL )
204+ }
205+ return nil , ErrUpstream (0 , "请求上游失败" , err )
206+ }
207+
208+ return resp , nil
209+ }
210+
211+ func codexVersionFromProfile (profile deviceProfile , fallback string ) string {
212+ if profile .HasVersion {
213+ return fmt .Sprintf ("%d.%d.%d" , profile .Version .major , profile .Version .minor , profile .Version .patch )
214+ }
215+ return strings .TrimSpace (fallback )
216+ }
217+
218+ func applyCodexRequestHeaders (req * http.Request , account * auth.Account , accessToken , cacheKey , apiKey string , deviceCfg * DeviceProfileConfig , downstreamHeaders http.Header ) {
219+ if req == nil {
220+ return
221+ }
222+
223+ accountID := ""
224+ if account != nil {
225+ account .Mu ().RLock ()
226+ accountID = account .AccountID
227+ account .Mu ().RUnlock ()
228+ }
229+
230+ var profile deviceProfile
231+ version := ""
197232 if IsDeviceProfileStabilizationEnabled (deviceCfg ) {
198- profile : = ResolveDeviceProfile (account , apiKey , headers , deviceCfg )
233+ profile = ResolveDeviceProfile (account , apiKey , downstreamHeaders , deviceCfg )
199234 ApplyDeviceProfileHeaders (req , profile )
200- // 稳定化时也需要设置 Version 头,保持行为一致
201- if profile .HasVersion {
202- req .Header .Set ("Version" , fmt .Sprintf ("%d.%d.%d" , profile .Version .major , profile .Version .minor , profile .Version .patch ))
203- }
235+ version = codexVersionFromProfile (profile , strings .TrimSpace (deviceCfg .PackageVersion ))
204236 } else {
205- // 每个账号使用确定性的 ClientProfile(UA + Version),模拟真实用户多样性
206- profile := ProfileForAccount (account .ID ())
207- req .Header .Set ("User-Agent" , profile .UserAgent )
208- req .Header .Set ("Version" , profile .Version )
237+ clientProfile := ProfileForAccount (account .ID ())
238+ req .Header .Set ("User-Agent" , clientProfile .UserAgent )
239+ version = clientProfile .Version
209240 }
210241
211242 req .Header .Set ("Authorization" , "Bearer " + accessToken )
212243 req .Header .Set ("Content-Type" , "application/json" )
213244 req .Header .Set ("Accept" , "text/event-stream" )
214- req .Header .Set ("Originator" , Originator )
215245 req .Header .Set ("Connection" , "Keep-Alive" )
246+ if version != "" {
247+ req .Header .Set ("Version" , version )
248+ }
249+ if originator := strings .TrimSpace (downstreamHeaders .Get ("Originator" )); originator != "" {
250+ req .Header .Set ("Originator" , originator )
251+ } else {
252+ req .Header .Set ("Originator" , Originator )
253+ }
216254 if accountID != "" {
217255 req .Header .Set ("Chatgpt-Account-Id" , accountID )
218256 }
219-
220- // Session/Conversation 头(用于 prompt cache 绑定)
221- // 参考 CLIProxyAPI: req.Header.Set("Conversation_id", cache.ID)
222- // 参考 sub2api: headers.Set("session_id", sessionResolution.SessionID)
223257 if cacheKey != "" {
224258 req .Header .Set ("Session_id" , cacheKey )
225- req .Header .Set ("Conversation_id" , cacheKey )
259+ req .Header .Del ("Conversation_id" )
226260 }
227-
228- // 获取连接池 HTTP 客户端(账号级隔离,复用 TCP/TLS 连接)
229- client := getPooledClient (account , proxyURL )
230-
231- resp , err := client .Do (req )
232- if err != nil {
233- if shouldRecyclePooledClient (err ) {
234- recyclePooledClient (account , proxyURL )
235- }
236- return nil , ErrUpstream (0 , "请求上游失败" , err )
237- }
238-
239- return resp , nil
240261}
241262
242263// ResolveSessionID 从下游请求提取或生成 session ID
243- // 优先级(参考 sub2api):
244- // 1. Header: session_id
245- // 2. Header: conversation_id
246- // 3. Body: prompt_cache_key
247- // 4. 基于 Bearer API Key 的确定性 UUID(参考 CLIProxyAPI)
248- func ResolveSessionID (authHeader string , body []byte ) string {
249- // 此函数由 handler 调用,将 gin.Context 的 header 传进来
250-
264+ // 优先级:
265+ // 1. Header: Session_id
266+ // 2. Header: Conversation_id
267+ // 3. Header: Idempotency-Key
268+ // 4. Body: prompt_cache_key
269+ // 5. 基于 Bearer API Key 的确定性 UUID
270+ func ResolveSessionID (headers http.Header , body []byte ) string {
271+ if headers != nil {
272+ if v := strings .TrimSpace (headers .Get ("Session_id" )); v != "" {
273+ return v
274+ }
275+ if v := strings .TrimSpace (headers .Get ("Conversation_id" )); v != "" {
276+ return v
277+ }
278+ if v := strings .TrimSpace (headers .Get ("Idempotency-Key" )); v != "" {
279+ return v
280+ }
281+ }
251282 // 优先从 body 的 prompt_cache_key 提取
252283 if v := strings .TrimSpace (gjson .GetBytes (body , "prompt_cache_key" ).String ()); v != "" {
253284 return v
254285 }
255286
256287 // 基于下游用户的 API Key 生成确定性 cache key(参考 CLIProxyAPI codex_executor.go:621)
288+ authHeader := ""
289+ if headers != nil {
290+ authHeader = headers .Get ("Authorization" )
291+ }
257292 apiKey := strings .TrimPrefix (authHeader , "Bearer " )
258293 apiKey = strings .TrimSpace (apiKey )
259294 if apiKey != "" {
0 commit comments