diff --git a/common/api_type.go b/common/api_type.go index c31f2e2c..f045866a 100644 --- a/common/api_type.go +++ b/common/api_type.go @@ -65,8 +65,6 @@ func ChannelType2APIType(channelType int) (int, bool) { apiType = constant.APITypeCoze case constant.ChannelTypeJimeng: apiType = constant.APITypeJimeng - case constant.ChannelTypeClaudeCode: - apiType = constant.APITypeClaudeCode } if apiType == -1 { return constant.APITypeOpenAI, false diff --git a/constant/api_type.go b/constant/api_type.go index bca5e311..6ba5f257 100644 --- a/constant/api_type.go +++ b/constant/api_type.go @@ -31,6 +31,5 @@ const ( APITypeXai APITypeCoze APITypeJimeng - APITypeClaudeCode APITypeDummy // this one is only for count, do not add any channel after this ) diff --git a/constant/channel.go b/constant/channel.go index cc71caf3..2e1cc5b0 100644 --- a/constant/channel.go +++ b/constant/channel.go @@ -50,7 +50,6 @@ const ( ChannelTypeKling = 50 ChannelTypeJimeng = 51 ChannelTypeVidu = 52 - ChannelTypeClaudeCode = 53 ChannelTypeDummy // this one is only for count, do not add any channel after this ) @@ -109,5 +108,4 @@ var ChannelBaseURLs = []string{ "https://api.klingai.com", //50 "https://visual.volcengineapi.com", //51 "https://api.vidu.cn", //52 - "https://api.anthropic.com", //53 } diff --git a/controller/claude_oauth.go b/controller/claude_oauth.go deleted file mode 100644 index de711b93..00000000 --- a/controller/claude_oauth.go +++ /dev/null @@ -1,73 +0,0 @@ -package controller - -import ( - "net/http" - "one-api/common" - "one-api/service" - - "github.com/gin-gonic/gin" -) - -// ExchangeCodeRequest 授权码交换请求 -type ExchangeCodeRequest struct { - AuthorizationCode string `json:"authorization_code" binding:"required"` - CodeVerifier string `json:"code_verifier" binding:"required"` - State string `json:"state" binding:"required"` -} - -// GenerateClaudeOAuthURL 生成Claude OAuth授权URL -func GenerateClaudeOAuthURL(c *gin.Context) { - params, err := service.GenerateOAuthParams() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "success": false, - "message": "生成OAuth授权URL失败: " + err.Error(), - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "生成OAuth授权URL成功", - "data": params, - }) -} - -// ExchangeClaudeOAuthCode 交换Claude OAuth授权码 -func ExchangeClaudeOAuthCode(c *gin.Context) { - var req ExchangeCodeRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "success": false, - "message": "请求参数错误: " + err.Error(), - }) - return - } - - // 解析授权码 - cleanedCode, err := service.ParseAuthorizationCode(req.AuthorizationCode) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "success": false, - "message": err.Error(), - }) - return - } - - // 交换token - tokenResult, err := service.ExchangeCode(cleanedCode, req.CodeVerifier, req.State, nil) - if err != nil { - common.SysError("Claude OAuth token exchange failed: " + err.Error()) - c.JSON(http.StatusInternalServerError, gin.H{ - "success": false, - "message": "授权码交换失败: " + err.Error(), - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "授权码交换成功", - "data": tokenResult, - }) -} diff --git a/go.mod b/go.mod index bae7a4e8..94873c88 100644 --- a/go.mod +++ b/go.mod @@ -87,7 +87,6 @@ require ( github.com/yusufpapurcu/wmi v1.2.3 // indirect golang.org/x/arch v0.12.0 // indirect golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect google.golang.org/protobuf v1.34.2 // indirect diff --git a/go.sum b/go.sum index 8ded1a03..74eecd4c 100644 --- a/go.sum +++ b/go.sum @@ -231,8 +231,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= diff --git a/main.go b/main.go index f49995c2..ca3da601 100644 --- a/main.go +++ b/main.go @@ -86,9 +86,6 @@ func main() { // 数据看板 go model.UpdateQuotaData() - // Start Claude Code token refresh scheduler - service.StartClaudeTokenRefreshScheduler() - if os.Getenv("CHANNEL_UPDATE_FREQUENCY") != "" { frequency, err := strconv.Atoi(os.Getenv("CHANNEL_UPDATE_FREQUENCY")) if err != nil { diff --git a/middleware/distributor.go b/middleware/distributor.go index fb4a6645..c7a55f4c 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -269,6 +269,9 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode if channel.ChannelInfo.IsMultiKey { common.SetContextKey(c, constant.ContextKeyChannelIsMultiKey, true) common.SetContextKey(c, constant.ContextKeyChannelMultiKeyIndex, index) + } else { + // 必须设置为 false,否则在重试到单个 key 的时候会导致日志显示错误 + common.SetContextKey(c, constant.ContextKeyChannelIsMultiKey, false) } // c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", key)) common.SetContextKey(c, constant.ContextKeyChannelKey, key) diff --git a/relay/channel/claude_code/adaptor.go b/relay/channel/claude_code/adaptor.go deleted file mode 100644 index a5926f9d..00000000 --- a/relay/channel/claude_code/adaptor.go +++ /dev/null @@ -1,163 +0,0 @@ -package claude_code - -import ( - "errors" - "fmt" - "io" - "net/http" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/claude" - relaycommon "one-api/relay/common" - "one-api/types" - "strings" - - "github.com/gin-gonic/gin" -) - -const ( - RequestModeCompletion = 1 - RequestModeMessage = 2 - DefaultSystemPrompt = "You are Claude Code, Anthropic's official CLI for Claude." -) - -type Adaptor struct { - RequestMode int -} - -func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) { - //TODO implement me - return nil, errors.New("not implemented") -} - -func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) { - // Use configured system prompt if available, otherwise use default - if info.ChannelSetting.SystemPrompt != "" { - request.System = info.ChannelSetting.SystemPrompt - } else { - request.System = DefaultSystemPrompt - } - - return request, nil -} - -func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { - return nil, errors.New("not implemented") -} - -func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { - return nil, errors.New("not implemented") -} - -func (a *Adaptor) Init(info *relaycommon.RelayInfo) { - if strings.HasPrefix(info.UpstreamModelName, "claude-2") || strings.HasPrefix(info.UpstreamModelName, "claude-instant") { - a.RequestMode = RequestModeCompletion - } else { - a.RequestMode = RequestModeMessage - } -} - -func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { - if a.RequestMode == RequestModeMessage { - return fmt.Sprintf("%s/v1/messages", info.BaseUrl), nil - } else { - return fmt.Sprintf("%s/v1/complete", info.BaseUrl), nil - } -} - -func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { - channel.SetupApiRequestHeader(info, c, req) - - // Parse accesstoken|refreshtoken format and use only the access token - accessToken := info.ApiKey - if strings.Contains(info.ApiKey, "|") { - parts := strings.Split(info.ApiKey, "|") - if len(parts) >= 1 { - accessToken = parts[0] - } - } - - // Claude Code specific headers - force override - req.Set("Authorization", "Bearer "+accessToken) - // 只有在没有设置的情况下才设置 anthropic-version - if req.Get("anthropic-version") == "" { - req.Set("anthropic-version", "2023-06-01") - } - req.Set("content-type", "application/json") - - // 只有在 user-agent 不包含 claude-cli 时才设置 - userAgent := req.Get("user-agent") - if userAgent == "" || !strings.Contains(strings.ToLower(userAgent), "claude-cli") { - req.Set("user-agent", "claude-cli/1.0.61 (external, cli)") - } - - // 只有在 anthropic-beta 不包含 claude-code 时才设置 - anthropicBeta := req.Get("anthropic-beta") - if anthropicBeta == "" || !strings.Contains(strings.ToLower(anthropicBeta), "claude-code") { - req.Set("anthropic-beta", "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14") - } - // if Anthropic-Dangerous-Direct-Browser-Access - anthropicDangerousDirectBrowserAccess := req.Get("anthropic-dangerous-direct-browser-access") - if anthropicDangerousDirectBrowserAccess == "" { - req.Set("anthropic-dangerous-direct-browser-access", "true") - } - - return nil -} - -func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - - if a.RequestMode == RequestModeCompletion { - return claude.RequestOpenAI2ClaudeComplete(*request), nil - } else { - claudeRequest, err := claude.RequestOpenAI2ClaudeMessage(*request) - if err != nil { - return nil, err - } - - // Use configured system prompt if available, otherwise use default - if info.ChannelSetting.SystemPrompt != "" { - claudeRequest.System = info.ChannelSetting.SystemPrompt - } else { - claudeRequest.System = DefaultSystemPrompt - } - - return claudeRequest, nil - } -} - -func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) { - return nil, nil -} - -func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { - return nil, errors.New("not implemented") -} - -func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) { - return nil, errors.New("not implemented") -} - -func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { - return channel.DoApiRequest(a, c, info, requestBody) -} - -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) { - if info.IsStream { - err, usage = claude.ClaudeStreamHandler(c, resp, info, a.RequestMode) - } else { - err, usage = claude.ClaudeHandler(c, resp, a.RequestMode, info) - } - return -} - -func (a *Adaptor) GetModelList() []string { - return ModelList -} - -func (a *Adaptor) GetChannelName() string { - return ChannelName -} diff --git a/relay/channel/claude_code/constants.go b/relay/channel/claude_code/constants.go deleted file mode 100644 index 82695be2..00000000 --- a/relay/channel/claude_code/constants.go +++ /dev/null @@ -1,14 +0,0 @@ -package claude_code - -var ModelList = []string{ - "claude-3-5-haiku-20241022", - "claude-3-5-sonnet-20241022", - "claude-3-7-sonnet-20250219", - "claude-3-7-sonnet-20250219-thinking", - "claude-sonnet-4-20250514", - "claude-sonnet-4-20250514-thinking", - "claude-opus-4-20250514", - "claude-opus-4-20250514-thinking", -} - -var ChannelName = "claude_code" diff --git a/relay/channel/claude_code/dto.go b/relay/channel/claude_code/dto.go deleted file mode 100644 index 68bb9269..00000000 --- a/relay/channel/claude_code/dto.go +++ /dev/null @@ -1,4 +0,0 @@ -package claude_code - -// Claude Code uses the same DTO structures as Claude since it's based on the same API -// This file is kept for consistency with the channel structure pattern \ No newline at end of file diff --git a/relay/channel/gemini/relay-gemini-native.go b/relay/channel/gemini/relay-gemini-native.go index 2060fd8c..1ba599b3 100644 --- a/relay/channel/gemini/relay-gemini-native.go +++ b/relay/channel/gemini/relay-gemini-native.go @@ -1,6 +1,7 @@ package gemini import ( + "github.com/pkg/errors" "io" "net/http" "one-api/common" @@ -110,10 +111,14 @@ func GeminiTextGenerationStreamHandler(c *gin.Context, info *relaycommon.RelayIn if err != nil { common.LogError(c, err.Error()) } - + info.SendResponseCount++ return true }) + if info.SendResponseCount == 0 { + return nil, types.NewOpenAIError(errors.New("no response received from Gemini API"), types.ErrorCodeEmptyResponse, http.StatusInternalServerError) + } + if imageCount != 0 { if usage.CompletionTokens == 0 { usage.CompletionTokens = imageCount * 258 diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index 4065259f..3fe41600 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -725,10 +725,9 @@ func responseGeminiChat2OpenAI(c *gin.Context, response *dto.GeminiChatResponse) return &fullTextResponse } -func streamResponseGeminiChat2OpenAI(geminiResponse *dto.GeminiChatResponse) (*dto.ChatCompletionsStreamResponse, bool, bool) { +func streamResponseGeminiChat2OpenAI(geminiResponse *dto.GeminiChatResponse) (*dto.ChatCompletionsStreamResponse, bool) { choices := make([]dto.ChatCompletionsStreamResponseChoice, 0, len(geminiResponse.Candidates)) isStop := false - hasImage := false for _, candidate := range geminiResponse.Candidates { if candidate.FinishReason != nil && *candidate.FinishReason == "STOP" { isStop = true @@ -759,7 +758,6 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *dto.GeminiChatResponse) (*d if strings.HasPrefix(part.InlineData.MimeType, "image") { imgText := "" texts = append(texts, imgText) - hasImage = true } } else if part.FunctionCall != nil { isTools = true @@ -796,7 +794,7 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *dto.GeminiChatResponse) (*d var response dto.ChatCompletionsStreamResponse response.Object = "chat.completion.chunk" response.Choices = choices - return &response, isStop, hasImage + return &response, isStop } func handleStream(c *gin.Context, info *relaycommon.RelayInfo, resp *dto.ChatCompletionsStreamResponse) error { @@ -824,11 +822,10 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp * // responseText := "" id := helper.GetResponseID(c) createAt := common.GetTimestamp() + responseText := strings.Builder{} var usage = &dto.Usage{} var imageCount int - respCount := 0 - helper.StreamScannerHandler(c, resp, info, func(data string) bool { var geminiResponse dto.GeminiChatResponse err := common.UnmarshalJsonStr(data, &geminiResponse) @@ -837,10 +834,19 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp * return false } - response, isStop, hasImage := streamResponseGeminiChat2OpenAI(&geminiResponse) - if hasImage { - imageCount++ + for _, candidate := range geminiResponse.Candidates { + for _, part := range candidate.Content.Parts { + if part.InlineData != nil && part.InlineData.MimeType != "" { + imageCount++ + } + if part.Text != "" { + responseText.WriteString(part.Text) + } + } } + + response, isStop := streamResponseGeminiChat2OpenAI(&geminiResponse) + response.Id = id response.Created = createAt response.Model = info.UpstreamModelName @@ -858,7 +864,7 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp * } } - if respCount == 0 { + if info.SendResponseCount == 0 { // send first response err = handleStream(c, info, helper.GenerateStartEmptyResponse(id, createAt, info.UpstreamModelName, nil)) if err != nil { @@ -873,11 +879,10 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp * if isStop { _ = handleStream(c, info, helper.GenerateStopResponse(id, createAt, info.UpstreamModelName, constant.FinishReasonStop)) } - respCount++ return true }) - if respCount == 0 { + if info.SendResponseCount == 0 { // 空补全,报错不计费 // empty response, throw an error return nil, types.NewOpenAIError(errors.New("no response received from Gemini API"), types.ErrorCodeEmptyResponse, http.StatusInternalServerError) @@ -892,6 +897,16 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp * usage.PromptTokensDetails.TextTokens = usage.PromptTokens usage.CompletionTokens = usage.TotalTokens - usage.PromptTokens + if usage.CompletionTokens == 0 { + str := responseText.String() + if len(str) > 0 { + usage = service.ResponseText2Usage(responseText.String(), info.UpstreamModelName, info.PromptTokens) + } else { + // 空补全,不需要使用量 + usage = &dto.Usage{} + } + } + response := helper.GenerateFinalUsageResponse(id, createAt, info.UpstreamModelName, *usage) err := handleFinalStream(c, info, response) if err != nil { diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go index 2456c77f..cc9c5bbb 100644 --- a/relay/relay_adaptor.go +++ b/relay/relay_adaptor.go @@ -9,7 +9,6 @@ import ( "one-api/relay/channel/baidu" "one-api/relay/channel/baidu_v2" "one-api/relay/channel/claude" - "one-api/relay/channel/claude_code" "one-api/relay/channel/cloudflare" "one-api/relay/channel/cohere" "one-api/relay/channel/coze" @@ -99,8 +98,6 @@ func GetAdaptor(apiType int) channel.Adaptor { return &coze.Adaptor{} case constant.APITypeJimeng: return &jimeng.Adaptor{} - case constant.APITypeClaudeCode: - return &claude_code.Adaptor{} } return nil } diff --git a/router/api-router.go b/router/api-router.go index 702fc99f..bc49803a 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -120,9 +120,6 @@ func SetApiRouter(router *gin.Engine) { channelRoute.POST("/batch/tag", controller.BatchSetChannelTag) channelRoute.GET("/tag/models", controller.GetTagModels) channelRoute.POST("/copy/:id", controller.CopyChannel) - // Claude OAuth路由 - channelRoute.GET("/claude/oauth/url", controller.GenerateClaudeOAuthURL) - channelRoute.POST("/claude/oauth/exchange", controller.ExchangeClaudeOAuthCode) } tokenRoute := apiRouter.Group("/token") tokenRoute.Use(middleware.UserAuth()) diff --git a/service/claude_oauth.go b/service/claude_oauth.go deleted file mode 100644 index b0e1f84d..00000000 --- a/service/claude_oauth.go +++ /dev/null @@ -1,171 +0,0 @@ -package service - -import ( - "context" - "fmt" - "net/http" - "os" - "strings" - "time" - - "golang.org/x/oauth2" -) - -const ( - // Default OAuth configuration values - DefaultAuthorizeURL = "https://claude.ai/oauth/authorize" - DefaultTokenURL = "https://console.anthropic.com/v1/oauth/token" - DefaultClientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e" - DefaultRedirectURI = "https://console.anthropic.com/oauth/code/callback" - DefaultScopes = "user:inference" -) - -// getOAuthValues returns OAuth configuration values from environment variables or defaults -func getOAuthValues() (authorizeURL, tokenURL, clientID, redirectURI, scopes string) { - authorizeURL = os.Getenv("CLAUDE_AUTHORIZE_URL") - if authorizeURL == "" { - authorizeURL = DefaultAuthorizeURL - } - - tokenURL = os.Getenv("CLAUDE_TOKEN_URL") - if tokenURL == "" { - tokenURL = DefaultTokenURL - } - - clientID = os.Getenv("CLAUDE_CLIENT_ID") - if clientID == "" { - clientID = DefaultClientID - } - - redirectURI = os.Getenv("CLAUDE_REDIRECT_URI") - if redirectURI == "" { - redirectURI = DefaultRedirectURI - } - - scopes = os.Getenv("CLAUDE_SCOPES") - if scopes == "" { - scopes = DefaultScopes - } - - return -} - -type OAuth2Credentials struct { - AuthURL string `json:"auth_url"` - CodeVerifier string `json:"code_verifier"` - State string `json:"state"` - CodeChallenge string `json:"code_challenge"` -} - -// GetClaudeOAuthConfig returns the Claude OAuth2 configuration -func GetClaudeOAuthConfig() *oauth2.Config { - authorizeURL, tokenURL, clientID, redirectURI, scopes := getOAuthValues() - - return &oauth2.Config{ - ClientID: clientID, - RedirectURL: redirectURI, - Scopes: strings.Split(scopes, " "), - Endpoint: oauth2.Endpoint{ - AuthURL: authorizeURL, - TokenURL: tokenURL, - }, - } -} - -// getOAuthConfig is kept for backward compatibility -func getOAuthConfig() *oauth2.Config { - return GetClaudeOAuthConfig() -} - -// GenerateOAuthParams generates OAuth authorization URL and related parameters -func GenerateOAuthParams() (*OAuth2Credentials, error) { - config := getOAuthConfig() - - // Generate PKCE parameters - codeVerifier := oauth2.GenerateVerifier() - state := oauth2.GenerateVerifier() // Reuse generator as state - - // Generate authorization URL - authURL := config.AuthCodeURL(state, - oauth2.S256ChallengeOption(codeVerifier), - oauth2.SetAuthURLParam("code", "true"), // Claude-specific parameter - ) - - return &OAuth2Credentials{ - AuthURL: authURL, - CodeVerifier: codeVerifier, - State: state, - CodeChallenge: oauth2.S256ChallengeFromVerifier(codeVerifier), - }, nil -} - -// ExchangeCode -func ExchangeCode(authorizationCode, codeVerifier, state string, client *http.Client) (*oauth2.Token, error) { - config := getOAuthConfig() - - if strings.Contains(authorizationCode, "#") { - parts := strings.Split(authorizationCode, "#") - if len(parts) > 0 { - authorizationCode = parts[0] - } - } - - ctx := context.Background() - if client != nil { - ctx = context.WithValue(ctx, oauth2.HTTPClient, client) - } - - token, err := config.Exchange(ctx, authorizationCode, - oauth2.VerifierOption(codeVerifier), - oauth2.SetAuthURLParam("state", state), - ) - if err != nil { - return nil, fmt.Errorf("token exchange failed: %w", err) - } - - return token, nil -} - -func ParseAuthorizationCode(input string) (string, error) { - if input == "" { - return "", fmt.Errorf("please provide a valid authorization code") - } - // URLs are not allowed - if strings.Contains(input, "http") || strings.Contains(input, "https") { - return "", fmt.Errorf("authorization code cannot contain URLs") - } - - return input, nil -} - -// GetClaudeHTTPClient returns a configured HTTP client for Claude OAuth operations -func GetClaudeHTTPClient() *http.Client { - return &http.Client{ - Timeout: 30 * time.Second, - } -} - -// RefreshClaudeToken refreshes a Claude OAuth token using the refresh token -func RefreshClaudeToken(accessToken, refreshToken string) (*oauth2.Token, error) { - config := GetClaudeOAuthConfig() - - // Create token from current values - currentToken := &oauth2.Token{ - AccessToken: accessToken, - RefreshToken: refreshToken, - TokenType: "Bearer", - } - - ctx := context.Background() - if client := GetClaudeHTTPClient(); client != nil { - ctx = context.WithValue(ctx, oauth2.HTTPClient, client) - } - - // Refresh the token - newToken, err := config.TokenSource(ctx, currentToken).Token() - if err != nil { - return nil, fmt.Errorf("failed to refresh Claude token: %w", err) - } - - return newToken, nil -} diff --git a/service/claude_token_refresh.go b/service/claude_token_refresh.go deleted file mode 100644 index 5dc35367..00000000 --- a/service/claude_token_refresh.go +++ /dev/null @@ -1,94 +0,0 @@ -package service - -import ( - "fmt" - "one-api/common" - "one-api/constant" - "one-api/model" - "strings" - "time" - - "github.com/bytedance/gopkg/util/gopool" -) - -// StartClaudeTokenRefreshScheduler starts the scheduled token refresh for Claude Code channels -func StartClaudeTokenRefreshScheduler() { - ticker := time.NewTicker(5 * time.Minute) - gopool.Go(func() { - defer ticker.Stop() - for range ticker.C { - RefreshClaudeCodeTokens() - } - }) - common.SysLog("Claude Code token refresh scheduler started (5 minute interval)") -} - -// RefreshClaudeCodeTokens refreshes tokens for all active Claude Code channels -func RefreshClaudeCodeTokens() { - var channels []model.Channel - - // Get all active Claude Code channels - err := model.DB.Where("type = ? AND status = ?", constant.ChannelTypeClaudeCode, common.ChannelStatusEnabled).Find(&channels).Error - if err != nil { - common.SysError("Failed to get Claude Code channels: " + err.Error()) - return - } - - refreshCount := 0 - for _, channel := range channels { - if refreshTokenForChannel(&channel) { - refreshCount++ - } - } - - if refreshCount > 0 { - common.SysLog(fmt.Sprintf("Successfully refreshed %d Claude Code channel tokens", refreshCount)) - } -} - -// refreshTokenForChannel attempts to refresh token for a single channel -func refreshTokenForChannel(channel *model.Channel) bool { - // Parse key in format: accesstoken|refreshtoken - if channel.Key == "" || !strings.Contains(channel.Key, "|") { - common.SysError(fmt.Sprintf("Channel %d has invalid key format, expected accesstoken|refreshtoken", channel.Id)) - return false - } - - parts := strings.Split(channel.Key, "|") - if len(parts) < 2 { - common.SysError(fmt.Sprintf("Channel %d has invalid key format, expected accesstoken|refreshtoken", channel.Id)) - return false - } - - accessToken := parts[0] - refreshToken := parts[1] - - if refreshToken == "" { - common.SysError(fmt.Sprintf("Channel %d has empty refresh token", channel.Id)) - return false - } - - // Check if token needs refresh (refresh 30 minutes before expiry) - // if !shouldRefreshToken(accessToken) { - // return false - // } - - // Use shared refresh function - newToken, err := RefreshClaudeToken(accessToken, refreshToken) - if err != nil { - common.SysError(fmt.Sprintf("Failed to refresh token for channel %d: %s", channel.Id, err.Error())) - return false - } - - // Update channel with new tokens - newKey := fmt.Sprintf("%s|%s", newToken.AccessToken, newToken.RefreshToken) - - err = model.DB.Model(channel).Update("key", newKey).Error - if err != nil { - common.SysError(fmt.Sprintf("Failed to update channel %d with new token: %s", channel.Id, err.Error())) - return false - } - - common.SysLog(fmt.Sprintf("Successfully refreshed token for Claude Code channel %d (%s)", channel.Id, channel.Name)) - return true -} diff --git a/setting/operation_setting/operation_setting.go b/setting/operation_setting/operation_setting.go index 29b77d66..ef330d1a 100644 --- a/setting/operation_setting/operation_setting.go +++ b/setting/operation_setting/operation_setting.go @@ -13,9 +13,6 @@ var AutomaticDisableKeywords = []string{ "The security token included in the request is invalid", "Operation not allowed", "Your account is not authorized", - // Claude Code - "Invalid bearer token", - "OAuth authentication is currently not allowed for this endpoint", } func AutomaticDisableKeywordsToString() string { diff --git a/web/src/components/common/JSONEditor.js b/web/src/components/common/JSONEditor.js index d0c159b2..649d5a58 100644 --- a/web/src/components/common/JSONEditor.js +++ b/web/src/components/common/JSONEditor.js @@ -65,7 +65,8 @@ const JSONEditor = ({ const keyCount = Object.keys(parsed).length; return keyCount > 10 ? 'manual' : 'visual'; } catch (error) { - return 'visual'; + // JSON无效时默认显示手动编辑模式 + return 'manual'; } } return 'visual'; @@ -201,6 +202,18 @@ const JSONEditor = ({ // 渲染键值对编辑器 const renderKeyValueEditor = () => { + if (typeof jsonData !== 'object' || jsonData === null) { + return ( +