package repository import ( "context" "net/http" "net/url" "strings" "time" infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/imroc/req/v3" ) // NewOpenAIOAuthClient creates a new OpenAI OAuth client func NewOpenAIOAuthClient() service.OpenAIOAuthClient { return &openaiOAuthService{tokenURL: openai.TokenURL} } type openaiOAuthService struct { tokenURL string } func (s *openaiOAuthService) ExchangeCode(ctx context.Context, code, codeVerifier, redirectURI, proxyURL, clientID string) (*openai.TokenResponse, error) { client := createOpenAIReqClient(proxyURL) if redirectURI == "" { redirectURI = openai.DefaultRedirectURI } clientID = strings.TrimSpace(clientID) if clientID == "" { clientID = openai.ClientID } formData := url.Values{} formData.Set("grant_type", "authorization_code") formData.Set("client_id", clientID) formData.Set("code", code) formData.Set("redirect_uri", redirectURI) formData.Set("code_verifier", codeVerifier) var tokenResp openai.TokenResponse resp, err := client.R(). SetContext(ctx). SetHeader("User-Agent", "codex-cli/0.91.0"). SetFormDataFromValues(formData). SetSuccessResult(&tokenResp). Post(s.tokenURL) if err != nil { return nil, infraerrors.Newf(http.StatusBadGateway, "OPENAI_OAUTH_REQUEST_FAILED", "request failed: %v", err) } if !resp.IsSuccessState() { return nil, infraerrors.Newf(http.StatusBadGateway, "OPENAI_OAUTH_TOKEN_EXCHANGE_FAILED", "token exchange failed: status %d, body: %s", resp.StatusCode, resp.String()) } return &tokenResp, nil } func (s *openaiOAuthService) RefreshToken(ctx context.Context, refreshToken, proxyURL string) (*openai.TokenResponse, error) { return s.RefreshTokenWithClientID(ctx, refreshToken, proxyURL, "") } func (s *openaiOAuthService) RefreshTokenWithClientID(ctx context.Context, refreshToken, proxyURL string, clientID string) (*openai.TokenResponse, error) { // 调用方应始终传入正确的 client_id;为兼容旧数据,未指定时默认使用 OpenAI ClientID clientID = strings.TrimSpace(clientID) if clientID == "" { clientID = openai.ClientID } return s.refreshTokenWithClientID(ctx, refreshToken, proxyURL, clientID) } func (s *openaiOAuthService) refreshTokenWithClientID(ctx context.Context, refreshToken, proxyURL, clientID string) (*openai.TokenResponse, error) { client := createOpenAIReqClient(proxyURL) formData := url.Values{} formData.Set("grant_type", "refresh_token") formData.Set("refresh_token", refreshToken) formData.Set("client_id", clientID) formData.Set("scope", openai.RefreshScopes) var tokenResp openai.TokenResponse resp, err := client.R(). SetContext(ctx). SetHeader("User-Agent", "codex-cli/0.91.0"). SetFormDataFromValues(formData). SetSuccessResult(&tokenResp). Post(s.tokenURL) if err != nil { return nil, infraerrors.Newf(http.StatusBadGateway, "OPENAI_OAUTH_REQUEST_FAILED", "request failed: %v", err) } if !resp.IsSuccessState() { return nil, infraerrors.Newf(http.StatusBadGateway, "OPENAI_OAUTH_TOKEN_REFRESH_FAILED", "token refresh failed: status %d, body: %s", resp.StatusCode, resp.String()) } return &tokenResp, nil } func createOpenAIReqClient(proxyURL string) *req.Client { return getSharedReqClient(reqClientOptions{ ProxyURL: proxyURL, Timeout: 120 * time.Second, }) }