mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-06 16:15:50 +08:00
Detect confirmed CLI, sdk-cli and VSCode callers before mutation so native software, system, tool, cache and beta shapes pass through, while unconfirmed OAuth clients receive a coherent minimum CLI identity. Persist each Claude OAuth credential's upstream account metadata and one stable device ID, derive one stable session per agent conversation, and keep body and header identity synchronized across Messages, streaming and count_tokens. Alias every cloaked third-party custom tool through caller-stable opaque MCP names and restore declarations, choices, history, references, non-stream responses and SSE events without changing tool ownership. Implement the Claude Code 2.1.220 CCH algorithm over the final serialized request bytes, align currentDate and first-user cache layout, update the official beta/header baseline, and use upstream count_tokens for OAuth and first-party Anthropic credentials. Match the 2.1.220 TLS ClientHello so the transport fingerprint agrees with the identity the request now claims, and document the CLI defaults and automatic OAuth signing / tool alias behaviour in config.example.yaml.
153 lines
4.9 KiB
Go
153 lines
4.9 KiB
Go
package executor
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
claudeauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
|
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
claudeAccountProfileCheckedAtKey = "claude_account_profile_checked_at"
|
|
claudeAccountProfileRefreshAge = 24 * time.Hour
|
|
claudeAccountProfileTimeout = 10 * time.Second
|
|
)
|
|
|
|
type claudeOAuthProfileFetcher func(context.Context, *cliproxyauth.Auth, string) (*claudeauth.OAuthProfile, error)
|
|
|
|
func (e *ClaudeExecutor) ShouldPrepareRequestAuth(auth *cliproxyauth.Auth) bool {
|
|
apiKey, _ := claudeCreds(auth)
|
|
if !isClaudeOAuthToken(apiKey) || auth == nil {
|
|
return false
|
|
}
|
|
if !claudeauth.HasCanonicalDeviceIDPool(auth.Metadata[claudeauth.ClaudeDeviceIDsMetadataKey]) {
|
|
return true
|
|
}
|
|
if helps.ClaudeCredentialAccountUUID(auth) != "" {
|
|
return false
|
|
}
|
|
return claudeAccountProfileLookupDue(auth.Metadata, time.Now())
|
|
}
|
|
|
|
func claudeAccountProfileLookupDue(metadata map[string]any, now time.Time) bool {
|
|
if metadata == nil {
|
|
return true
|
|
}
|
|
checkedAt, _ := metadata[claudeAccountProfileCheckedAtKey].(string)
|
|
checkedAt = strings.TrimSpace(checkedAt)
|
|
if checkedAt == "" {
|
|
return true
|
|
}
|
|
parsed, errParse := time.Parse(time.RFC3339, checkedAt)
|
|
return errParse != nil || !parsed.Add(claudeAccountProfileRefreshAge).After(now)
|
|
}
|
|
|
|
func (e *ClaudeExecutor) PrepareRequestAuth(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
|
|
if auth == nil || !e.ShouldPrepareRequestAuth(auth) {
|
|
return auth, nil
|
|
}
|
|
apiKey, _ := claudeCreds(auth)
|
|
if auth.Metadata == nil {
|
|
auth.Metadata = make(map[string]any)
|
|
}
|
|
if _, errDeviceIDs := helps.EnsureClaudeCredentialDevicePoolRequired(ctx, auth); errDeviceIDs != nil {
|
|
return nil, errDeviceIDs
|
|
}
|
|
if helps.ClaudeCredentialAccountUUID(auth) != "" || !claudeAccountProfileLookupDue(auth.Metadata, time.Now()) {
|
|
return auth, nil
|
|
}
|
|
|
|
auth.Metadata[claudeAccountProfileCheckedAtKey] = time.Now().UTC().Format(time.RFC3339)
|
|
profile, errProfile := e.fetchClaudeOAuthProfile(ctx, auth, apiKey)
|
|
if errProfile != nil {
|
|
if errContext := ctx.Err(); errContext != nil {
|
|
return nil, errContext
|
|
}
|
|
log.WithError(errProfile).Warn("claude executor: unable to populate OAuth account profile")
|
|
return auth, nil
|
|
}
|
|
if profile == nil {
|
|
return auth, nil
|
|
}
|
|
if accountUUID := strings.TrimSpace(profile.Account.UUID); accountUUID != "" {
|
|
auth.Metadata["account_uuid"] = accountUUID
|
|
}
|
|
if email := strings.TrimSpace(profile.Account.Email); email != "" {
|
|
auth.Metadata["email"] = email
|
|
}
|
|
if organizationUUID := strings.TrimSpace(profile.Organization.UUID); organizationUUID != "" {
|
|
auth.Metadata["organization_uuid"] = organizationUUID
|
|
}
|
|
if organizationName := strings.TrimSpace(profile.Organization.Name); organizationName != "" {
|
|
auth.Metadata["organization_name"] = organizationName
|
|
}
|
|
return auth, nil
|
|
}
|
|
|
|
func (e *ClaudeExecutor) fetchClaudeOAuthProfile(ctx context.Context, auth *cliproxyauth.Auth, apiKey string) (*claudeauth.OAuthProfile, error) {
|
|
if e == nil {
|
|
return nil, fmt.Errorf("fetch Claude OAuth profile: executor is nil")
|
|
}
|
|
if e.oauthProfileFetcher != nil {
|
|
return e.oauthProfileFetcher(ctx, auth, apiKey)
|
|
}
|
|
if auth == nil {
|
|
return nil, fmt.Errorf("fetch Claude OAuth profile: auth is nil")
|
|
}
|
|
profileCtx, cancelProfile := context.WithTimeout(ctx, claudeAccountProfileTimeout)
|
|
defer cancelProfile()
|
|
service := claudeauth.NewClaudeAuthWithProxyURL(e.cfg, auth.ProxyURL)
|
|
return service.FetchOAuthProfile(profileCtx, apiKey)
|
|
}
|
|
|
|
func (e *ClaudeExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
|
|
log.Debugf("claude executor: refresh called")
|
|
if refreshed, handled, err := helps.RefreshAuthViaHome(ctx, e.cfg, auth); handled {
|
|
return refreshed, err
|
|
}
|
|
if auth == nil {
|
|
return nil, fmt.Errorf("claude executor: auth is nil")
|
|
}
|
|
var refreshToken string
|
|
if auth.Metadata != nil {
|
|
if v, ok := auth.Metadata["refresh_token"].(string); ok && v != "" {
|
|
refreshToken = v
|
|
}
|
|
}
|
|
if refreshToken == "" {
|
|
return auth, nil
|
|
}
|
|
svc := claudeauth.NewClaudeAuthWithProxyURL(e.cfg, auth.ProxyURL)
|
|
td, err := svc.RefreshTokensWithRetry(ctx, refreshToken, 3)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if auth.Metadata == nil {
|
|
auth.Metadata = make(map[string]any)
|
|
}
|
|
auth.Metadata["access_token"] = td.AccessToken
|
|
if td.RefreshToken != "" {
|
|
auth.Metadata["refresh_token"] = td.RefreshToken
|
|
}
|
|
auth.Metadata["email"] = td.Email
|
|
if td.AccountUUID != "" {
|
|
auth.Metadata["account_uuid"] = td.AccountUUID
|
|
}
|
|
if td.OrganizationUUID != "" {
|
|
auth.Metadata["organization_uuid"] = td.OrganizationUUID
|
|
}
|
|
if td.OrganizationName != "" {
|
|
auth.Metadata["organization_name"] = td.OrganizationName
|
|
}
|
|
auth.Metadata["expired"] = td.Expire
|
|
auth.Metadata["type"] = "claude"
|
|
now := time.Now().Format(time.RFC3339)
|
|
auth.Metadata["last_refresh"] = now
|
|
return auth, nil
|
|
}
|