2025-12-22 22:58:31 +08:00
package repository
import (
"context"
2026-04-23 12:23:04 +08:00
"errors"
2026-01-27 19:13:01 +08:00
"net/http"
2025-12-22 22:58:31 +08:00
"net/url"
2026-02-19 08:02:56 +08:00
"strings"
2025-12-22 22:58:31 +08:00
"time"
2026-01-27 19:13:01 +08:00
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
2025-12-24 21:07:21 +08:00
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
2025-12-25 17:15:01 +08:00
"github.com/Wei-Shaw/sub2api/internal/service"
2025-12-22 22:58:31 +08:00
"github.com/imroc/req/v3"
)
// NewOpenAIOAuthClient creates a new OpenAI OAuth client
2025-12-25 17:15:01 +08:00
func NewOpenAIOAuthClient ( ) service . OpenAIOAuthClient {
2025-12-25 10:52:56 +08:00
return & openaiOAuthService { tokenURL : openai . TokenURL }
}
type openaiOAuthService struct {
tokenURL string
2025-12-22 22:58:31 +08:00
}
2026-02-28 15:01:20 +08:00
func ( s * openaiOAuthService ) ExchangeCode ( ctx context . Context , code , codeVerifier , redirectURI , proxyURL , clientID string ) ( * openai . TokenResponse , error ) {
2026-03-02 15:53:26 +08:00
client , err := createOpenAIReqClient ( proxyURL )
if err != nil {
return nil , infraerrors . Newf ( http . StatusBadGateway , "OPENAI_OAUTH_CLIENT_INIT_FAILED" , "create HTTP client: %v" , err )
}
2025-12-22 22:58:31 +08:00
if redirectURI == "" {
redirectURI = openai . DefaultRedirectURI
}
2026-02-28 15:01:20 +08:00
clientID = strings . TrimSpace ( clientID )
if clientID == "" {
clientID = openai . ClientID
}
2025-12-22 22:58:31 +08:00
formData := url . Values { }
formData . Set ( "grant_type" , "authorization_code" )
2026-02-28 15:01:20 +08:00
formData . Set ( "client_id" , clientID )
2025-12-22 22:58:31 +08:00
formData . Set ( "code" , code )
formData . Set ( "redirect_uri" , redirectURI )
formData . Set ( "code_verifier" , codeVerifier )
var tokenResp openai . TokenResponse
resp , err := client . R ( ) .
SetContext ( ctx ) .
2026-01-27 19:13:01 +08:00
SetHeader ( "User-Agent" , "codex-cli/0.91.0" ) .
2025-12-22 22:58:31 +08:00
SetFormDataFromValues ( formData ) .
SetSuccessResult ( & tokenResp ) .
2025-12-25 10:52:56 +08:00
Post ( s . tokenURL )
2025-12-22 22:58:31 +08:00
if err != nil {
2026-04-23 12:23:04 +08:00
if shouldReturnOpenAINoProxyHint ( ctx , proxyURL , err ) {
return nil , newOpenAINoProxyHintError ( err )
}
2026-01-27 19:13:01 +08:00
return nil , infraerrors . Newf ( http . StatusBadGateway , "OPENAI_OAUTH_REQUEST_FAILED" , "request failed: %v" , err )
2025-12-22 22:58:31 +08:00
}
if ! resp . IsSuccessState ( ) {
2026-01-27 19:13:01 +08:00
return nil , infraerrors . Newf ( http . StatusBadGateway , "OPENAI_OAUTH_TOKEN_EXCHANGE_FAILED" , "token exchange failed: status %d, body: %s" , resp . StatusCode , resp . String ( ) )
2025-12-22 22:58:31 +08:00
}
return & tokenResp , nil
}
func ( s * openaiOAuthService ) RefreshToken ( ctx context . Context , refreshToken , proxyURL string ) ( * openai . TokenResponse , error ) {
2026-02-19 08:02:56 +08:00
return s . RefreshTokenWithClientID ( ctx , refreshToken , proxyURL , "" )
}
func ( s * openaiOAuthService ) RefreshTokenWithClientID ( ctx context . Context , refreshToken , proxyURL string , clientID string ) ( * openai . TokenResponse , error ) {
2026-02-28 15:01:20 +08:00
// 调用方应始终传入正确的 client_id; 为兼容旧数据, 未指定时默认使用 OpenAI ClientID
clientID = strings . TrimSpace ( clientID )
if clientID == "" {
clientID = openai . ClientID
2026-02-19 08:02:56 +08:00
}
2026-02-28 15:01:20 +08:00
return s . refreshTokenWithClientID ( ctx , refreshToken , proxyURL , clientID )
2026-02-19 08:02:56 +08:00
}
func ( s * openaiOAuthService ) refreshTokenWithClientID ( ctx context . Context , refreshToken , proxyURL , clientID string ) ( * openai . TokenResponse , error ) {
2026-03-02 15:53:26 +08:00
client , err := createOpenAIReqClient ( proxyURL )
if err != nil {
return nil , infraerrors . Newf ( http . StatusBadGateway , "OPENAI_OAUTH_CLIENT_INIT_FAILED" , "create HTTP client: %v" , err )
}
2025-12-22 22:58:31 +08:00
formData := url . Values { }
formData . Set ( "grant_type" , "refresh_token" )
formData . Set ( "refresh_token" , refreshToken )
2026-02-19 08:02:56 +08:00
formData . Set ( "client_id" , clientID )
2025-12-22 22:58:31 +08:00
formData . Set ( "scope" , openai . RefreshScopes )
var tokenResp openai . TokenResponse
resp , err := client . R ( ) .
SetContext ( ctx ) .
2026-01-27 19:13:01 +08:00
SetHeader ( "User-Agent" , "codex-cli/0.91.0" ) .
2025-12-22 22:58:31 +08:00
SetFormDataFromValues ( formData ) .
SetSuccessResult ( & tokenResp ) .
2025-12-25 10:52:56 +08:00
Post ( s . tokenURL )
2025-12-22 22:58:31 +08:00
if err != nil {
2026-04-23 12:23:04 +08:00
if shouldReturnOpenAINoProxyHint ( ctx , proxyURL , err ) {
return nil , newOpenAINoProxyHintError ( err )
}
2026-01-27 19:13:01 +08:00
return nil , infraerrors . Newf ( http . StatusBadGateway , "OPENAI_OAUTH_REQUEST_FAILED" , "request failed: %v" , err )
2025-12-22 22:58:31 +08:00
}
if ! resp . IsSuccessState ( ) {
2026-01-27 19:13:01 +08:00
return nil , infraerrors . Newf ( http . StatusBadGateway , "OPENAI_OAUTH_TOKEN_REFRESH_FAILED" , "token refresh failed: status %d, body: %s" , resp . StatusCode , resp . String ( ) )
2025-12-22 22:58:31 +08:00
}
return & tokenResp , nil
}
2026-03-02 15:53:26 +08:00
func createOpenAIReqClient ( proxyURL string ) ( * req . Client , error ) {
2025-12-31 08:50:12 +08:00
return getSharedReqClient ( reqClientOptions {
2026-01-27 19:13:01 +08:00
ProxyURL : proxyURL ,
Timeout : 120 * time . Second ,
2025-12-31 08:50:12 +08:00
} )
2025-12-22 22:58:31 +08:00
}
2026-04-23 12:23:04 +08:00
func shouldReturnOpenAINoProxyHint ( ctx context . Context , proxyURL string , err error ) bool {
if strings . TrimSpace ( proxyURL ) != "" || err == nil {
return false
}
if ctx != nil && ctx . Err ( ) != nil {
return false
}
return ! errors . Is ( err , context . Canceled )
}
func newOpenAINoProxyHintError ( cause error ) error {
return infraerrors . New (
http . StatusBadGateway ,
"OPENAI_OAUTH_PROXY_REQUIRED" ,
"OpenAI OAuth request failed: no proxy is configured and this server could not reach OpenAI directly. Select a proxy that can access OpenAI, then retry; if the authorization code has expired, regenerate the authorization URL." ,
) . WithCause ( cause )
}