Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
bb500b7b
Unverified
Commit
bb500b7b
authored
Dec 19, 2025
by
Wesley Liddick
Committed by
GitHub
Dec 19, 2025
Browse files
Merge pull request #9 from NepetaLemon/refactor/add-http-service-ports
refactor(backend): service http ports
parents
5c2e7ae2
cceada7d
Changes
22
Hide whitespace changes
Inline
Side-by-side
backend/cmd/server/wire.go
View file @
bb500b7b
...
@@ -40,6 +40,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -40,6 +40,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
// 服务器层 ProviderSet
// 服务器层 ProviderSet
server
.
ProviderSet
,
server
.
ProviderSet
,
// BuildInfo provider
provideServiceBuildInfo
,
// 清理函数提供者
// 清理函数提供者
provideCleanup
,
provideCleanup
,
...
@@ -49,6 +52,13 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -49,6 +52,13 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
return
nil
,
nil
return
nil
,
nil
}
}
func
provideServiceBuildInfo
(
buildInfo
handler
.
BuildInfo
)
service
.
BuildInfo
{
return
service
.
BuildInfo
{
Version
:
buildInfo
.
Version
,
BuildType
:
buildInfo
.
BuildType
,
}
}
func
provideCleanup
(
func
provideCleanup
(
db
*
gorm
.
DB
,
db
*
gorm
.
DB
,
rdb
*
redis
.
Client
,
rdb
*
redis
.
Client
,
...
...
backend/cmd/server/wire_gen.go
View file @
bb500b7b
...
@@ -43,7 +43,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -43,7 +43,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
client
:=
infrastructure
.
ProvideRedis
(
configConfig
)
client
:=
infrastructure
.
ProvideRedis
(
configConfig
)
emailCache
:=
repository
.
NewEmailCache
(
client
)
emailCache
:=
repository
.
NewEmailCache
(
client
)
emailService
:=
service
.
NewEmailService
(
settingRepository
,
emailCache
)
emailService
:=
service
.
NewEmailService
(
settingRepository
,
emailCache
)
turnstileService
:=
service
.
NewTurnstileService
(
settingService
)
turnstileVerifier
:=
repository
.
NewTurnstileVerifier
()
turnstileService
:=
service
.
NewTurnstileService
(
settingService
,
turnstileVerifier
)
emailQueueService
:=
service
.
ProvideEmailQueueService
(
emailService
)
emailQueueService
:=
service
.
ProvideEmailQueueService
(
emailService
)
authService
:=
service
.
NewAuthService
(
userRepository
,
configConfig
,
settingService
,
emailService
,
turnstileService
,
emailQueueService
)
authService
:=
service
.
NewAuthService
(
userRepository
,
configConfig
,
settingService
,
emailService
,
turnstileService
,
emailQueueService
)
authHandler
:=
handler
.
NewAuthHandler
(
authService
)
authHandler
:=
handler
.
NewAuthHandler
(
authService
)
...
@@ -68,32 +69,41 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -68,32 +69,41 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
subscriptionHandler
:=
handler
.
NewSubscriptionHandler
(
subscriptionService
)
subscriptionHandler
:=
handler
.
NewSubscriptionHandler
(
subscriptionService
)
accountRepository
:=
repository
.
NewAccountRepository
(
db
)
accountRepository
:=
repository
.
NewAccountRepository
(
db
)
proxyRepository
:=
repository
.
NewProxyRepository
(
db
)
proxyRepository
:=
repository
.
NewProxyRepository
(
db
)
adminService
:=
service
.
NewAdminService
(
userRepository
,
groupRepository
,
accountRepository
,
proxyRepository
,
apiKeyRepository
,
redeemCodeRepository
,
usageLogRepository
,
userSubscriptionRepository
,
billingCacheService
)
proxyExitInfoProber
:=
repository
.
NewProxyExitInfoProber
()
adminService
:=
service
.
NewAdminService
(
userRepository
,
groupRepository
,
accountRepository
,
proxyRepository
,
apiKeyRepository
,
redeemCodeRepository
,
usageLogRepository
,
userSubscriptionRepository
,
billingCacheService
,
proxyExitInfoProber
)
dashboardHandler
:=
admin
.
NewDashboardHandler
(
adminService
,
usageLogRepository
)
dashboardHandler
:=
admin
.
NewDashboardHandler
(
adminService
,
usageLogRepository
)
adminUserHandler
:=
admin
.
NewUserHandler
(
adminService
)
adminUserHandler
:=
admin
.
NewUserHandler
(
adminService
)
groupHandler
:=
admin
.
NewGroupHandler
(
adminService
)
groupHandler
:=
admin
.
NewGroupHandler
(
adminService
)
oAuthService
:=
service
.
NewOAuthService
(
proxyRepository
)
claudeOAuthClient
:=
repository
.
NewClaudeOAuthClient
()
oAuthService
:=
service
.
NewOAuthService
(
proxyRepository
,
claudeOAuthClient
)
rateLimitService
:=
service
.
NewRateLimitService
(
accountRepository
,
configConfig
)
rateLimitService
:=
service
.
NewRateLimitService
(
accountRepository
,
configConfig
)
accountUsageService
:=
service
.
NewAccountUsageService
(
accountRepository
,
usageLogRepository
,
oAuthService
)
claudeUsageFetcher
:=
repository
.
NewClaudeUsageFetcher
()
accountTestService
:=
service
.
NewAccountTestService
(
accountRepository
,
oAuthService
)
accountUsageService
:=
service
.
NewAccountUsageService
(
accountRepository
,
usageLogRepository
,
oAuthService
,
claudeUsageFetcher
)
claudeUpstream
:=
repository
.
NewClaudeUpstream
(
configConfig
)
accountTestService
:=
service
.
NewAccountTestService
(
accountRepository
,
oAuthService
,
claudeUpstream
)
accountHandler
:=
admin
.
NewAccountHandler
(
adminService
,
oAuthService
,
rateLimitService
,
accountUsageService
,
accountTestService
)
accountHandler
:=
admin
.
NewAccountHandler
(
adminService
,
oAuthService
,
rateLimitService
,
accountUsageService
,
accountTestService
)
oAuthHandler
:=
admin
.
NewOAuthHandler
(
oAuthService
,
adminService
)
oAuthHandler
:=
admin
.
NewOAuthHandler
(
oAuthService
,
adminService
)
proxyHandler
:=
admin
.
NewProxyHandler
(
adminService
)
proxyHandler
:=
admin
.
NewProxyHandler
(
adminService
)
adminRedeemHandler
:=
admin
.
NewRedeemHandler
(
adminService
)
adminRedeemHandler
:=
admin
.
NewRedeemHandler
(
adminService
)
settingHandler
:=
admin
.
NewSettingHandler
(
settingService
,
emailService
)
settingHandler
:=
admin
.
NewSettingHandler
(
settingService
,
emailService
)
systemHandler
:=
handler
.
ProvideSystemHandler
(
client
,
buildInfo
)
updateCache
:=
repository
.
NewUpdateCache
(
client
)
gitHubReleaseClient
:=
repository
.
NewGitHubReleaseClient
()
serviceBuildInfo
:=
provideServiceBuildInfo
(
buildInfo
)
updateService
:=
service
.
ProvideUpdateService
(
updateCache
,
gitHubReleaseClient
,
serviceBuildInfo
)
systemHandler
:=
handler
.
ProvideSystemHandler
(
updateService
)
adminSubscriptionHandler
:=
admin
.
NewSubscriptionHandler
(
subscriptionService
)
adminSubscriptionHandler
:=
admin
.
NewSubscriptionHandler
(
subscriptionService
)
adminUsageHandler
:=
admin
.
NewUsageHandler
(
usageLogRepository
,
apiKeyRepository
,
usageService
,
adminService
)
adminUsageHandler
:=
admin
.
NewUsageHandler
(
usageLogRepository
,
apiKeyRepository
,
usageService
,
adminService
)
adminHandlers
:=
handler
.
ProvideAdminHandlers
(
dashboardHandler
,
adminUserHandler
,
groupHandler
,
accountHandler
,
oAuthHandler
,
proxyHandler
,
adminRedeemHandler
,
settingHandler
,
systemHandler
,
adminSubscriptionHandler
,
adminUsageHandler
)
adminHandlers
:=
handler
.
ProvideAdminHandlers
(
dashboardHandler
,
adminUserHandler
,
groupHandler
,
accountHandler
,
oAuthHandler
,
proxyHandler
,
adminRedeemHandler
,
settingHandler
,
systemHandler
,
adminSubscriptionHandler
,
adminUsageHandler
)
gatewayCache
:=
repository
.
NewGatewayCache
(
client
)
gatewayCache
:=
repository
.
NewGatewayCache
(
client
)
pricingService
,
err
:=
service
.
ProvidePricingService
(
configConfig
)
pricingRemoteClient
:=
repository
.
NewPricingRemoteClient
()
pricingService
,
err
:=
service
.
ProvidePricingService
(
configConfig
,
pricingRemoteClient
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
billingService
:=
service
.
NewBillingService
(
configConfig
,
pricingService
)
billingService
:=
service
.
NewBillingService
(
configConfig
,
pricingService
)
identityCache
:=
repository
.
NewIdentityCache
(
client
)
identityCache
:=
repository
.
NewIdentityCache
(
client
)
identityService
:=
service
.
NewIdentityService
(
identityCache
)
identityService
:=
service
.
NewIdentityService
(
identityCache
)
gatewayService
:=
service
.
NewGatewayService
(
accountRepository
,
usageLogRepository
,
userRepository
,
userSubscriptionRepository
,
gatewayCache
,
configConfig
,
oAuthService
,
billingService
,
rateLimitService
,
billingCacheService
,
identityService
)
gatewayService
:=
service
.
NewGatewayService
(
accountRepository
,
usageLogRepository
,
userRepository
,
userSubscriptionRepository
,
gatewayCache
,
configConfig
,
oAuthService
,
billingService
,
rateLimitService
,
billingCacheService
,
identityService
,
claudeUpstream
)
concurrencyCache
:=
repository
.
NewConcurrencyCache
(
client
)
concurrencyCache
:=
repository
.
NewConcurrencyCache
(
client
)
concurrencyService
:=
service
.
NewConcurrencyService
(
concurrencyCache
)
concurrencyService
:=
service
.
NewConcurrencyService
(
concurrencyCache
)
gatewayHandler
:=
handler
.
NewGatewayHandler
(
gatewayService
,
userService
,
concurrencyService
,
billingCacheService
)
gatewayHandler
:=
handler
.
NewGatewayHandler
(
gatewayService
,
userService
,
concurrencyService
,
billingCacheService
)
...
@@ -127,6 +137,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -127,6 +137,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
Subscription
:
subscriptionService
,
Subscription
:
subscriptionService
,
Concurrency
:
concurrencyService
,
Concurrency
:
concurrencyService
,
Identity
:
identityService
,
Identity
:
identityService
,
Update
:
updateService
,
}
}
repositories
:=
&
repository
.
Repositories
{
repositories
:=
&
repository
.
Repositories
{
User
:
userRepository
,
User
:
userRepository
,
...
@@ -156,6 +167,13 @@ type Application struct {
...
@@ -156,6 +167,13 @@ type Application struct {
Cleanup
func
()
Cleanup
func
()
}
}
func
provideServiceBuildInfo
(
buildInfo
handler
.
BuildInfo
)
service
.
BuildInfo
{
return
service
.
BuildInfo
{
Version
:
buildInfo
.
Version
,
BuildType
:
buildInfo
.
BuildType
,
}
}
func
provideCleanup
(
func
provideCleanup
(
db
*
gorm
.
DB
,
db
*
gorm
.
DB
,
rdb
*
redis
.
Client
,
rdb
*
redis
.
Client
,
...
...
backend/internal/handler/admin/system_handler.go
View file @
bb500b7b
...
@@ -6,11 +6,9 @@ import (
...
@@ -6,11 +6,9 @@ import (
"sub2api/internal/pkg/response"
"sub2api/internal/pkg/response"
"sub2api/internal/pkg/sysutil"
"sub2api/internal/pkg/sysutil"
"sub2api/internal/repository"
"sub2api/internal/service"
"sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
)
)
// SystemHandler handles system-related operations
// SystemHandler handles system-related operations
...
@@ -19,10 +17,9 @@ type SystemHandler struct {
...
@@ -19,10 +17,9 @@ type SystemHandler struct {
}
}
// NewSystemHandler creates a new SystemHandler
// NewSystemHandler creates a new SystemHandler
func
NewSystemHandler
(
rdb
*
redis
.
Client
,
version
,
buildType
string
)
*
SystemHandler
{
func
NewSystemHandler
(
updateSvc
*
service
.
UpdateService
)
*
SystemHandler
{
updateCache
:=
repository
.
NewUpdateCache
(
rdb
)
return
&
SystemHandler
{
return
&
SystemHandler
{
updateSvc
:
service
.
NewUpdateService
(
updateCache
,
version
,
buildType
)
,
updateSvc
:
updateSvc
,
}
}
}
}
...
...
backend/internal/handler/wire.go
View file @
bb500b7b
...
@@ -5,7 +5,6 @@ import (
...
@@ -5,7 +5,6 @@ import (
"sub2api/internal/service"
"sub2api/internal/service"
"github.com/google/wire"
"github.com/google/wire"
"github.com/redis/go-redis/v9"
)
)
// ProvideAdminHandlers creates the AdminHandlers struct
// ProvideAdminHandlers creates the AdminHandlers struct
...
@@ -37,9 +36,9 @@ func ProvideAdminHandlers(
...
@@ -37,9 +36,9 @@ func ProvideAdminHandlers(
}
}
}
}
// ProvideSystemHandler creates admin.SystemHandler with
BuildInfo parameters
// ProvideSystemHandler creates admin.SystemHandler with
UpdateService
func
ProvideSystemHandler
(
rdb
*
redis
.
Client
,
buildInfo
BuildInfo
)
*
admin
.
SystemHandler
{
func
ProvideSystemHandler
(
updateService
*
service
.
UpdateService
)
*
admin
.
SystemHandler
{
return
admin
.
NewSystemHandler
(
rdb
,
buildInfo
.
Version
,
buildInfo
.
BuildTyp
e
)
return
admin
.
NewSystemHandler
(
updateServic
e
)
}
}
// ProvideSettingHandler creates SettingHandler with version from BuildInfo
// ProvideSettingHandler creates SettingHandler with version from BuildInfo
...
...
backend/internal/repository/claude_oauth_service.go
0 → 100644
View file @
bb500b7b
package
repository
import
(
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"time"
"sub2api/internal/pkg/oauth"
"sub2api/internal/service"
"github.com/imroc/req/v3"
)
type
claudeOAuthService
struct
{}
func
NewClaudeOAuthClient
()
service
.
ClaudeOAuthClient
{
return
&
claudeOAuthService
{}
}
func
(
s
*
claudeOAuthService
)
GetOrganizationUUID
(
ctx
context
.
Context
,
sessionKey
,
proxyURL
string
)
(
string
,
error
)
{
client
:=
createReqClient
(
proxyURL
)
var
orgs
[]
struct
{
UUID
string
`json:"uuid"`
}
targetURL
:=
"https://claude.ai/api/organizations"
log
.
Printf
(
"[OAuth] Step 1: Getting organization UUID from %s"
,
targetURL
)
resp
,
err
:=
client
.
R
()
.
SetContext
(
ctx
)
.
SetCookies
(
&
http
.
Cookie
{
Name
:
"sessionKey"
,
Value
:
sessionKey
,
})
.
SetSuccessResult
(
&
orgs
)
.
Get
(
targetURL
)
if
err
!=
nil
{
log
.
Printf
(
"[OAuth] Step 1 FAILED - Request error: %v"
,
err
)
return
""
,
fmt
.
Errorf
(
"request failed: %w"
,
err
)
}
log
.
Printf
(
"[OAuth] Step 1 Response - Status: %d, Body: %s"
,
resp
.
StatusCode
,
resp
.
String
())
if
!
resp
.
IsSuccessState
()
{
return
""
,
fmt
.
Errorf
(
"failed to get organizations: status %d, body: %s"
,
resp
.
StatusCode
,
resp
.
String
())
}
if
len
(
orgs
)
==
0
{
return
""
,
fmt
.
Errorf
(
"no organizations found"
)
}
log
.
Printf
(
"[OAuth] Step 1 SUCCESS - Got org UUID: %s"
,
orgs
[
0
]
.
UUID
)
return
orgs
[
0
]
.
UUID
,
nil
}
func
(
s
*
claudeOAuthService
)
GetAuthorizationCode
(
ctx
context
.
Context
,
sessionKey
,
orgUUID
,
scope
,
codeChallenge
,
state
,
proxyURL
string
)
(
string
,
error
)
{
client
:=
createReqClient
(
proxyURL
)
authURL
:=
fmt
.
Sprintf
(
"https://claude.ai/v1/oauth/%s/authorize"
,
orgUUID
)
reqBody
:=
map
[
string
]
interface
{}{
"response_type"
:
"code"
,
"client_id"
:
oauth
.
ClientID
,
"organization_uuid"
:
orgUUID
,
"redirect_uri"
:
oauth
.
RedirectURI
,
"scope"
:
scope
,
"state"
:
state
,
"code_challenge"
:
codeChallenge
,
"code_challenge_method"
:
"S256"
,
}
reqBodyJSON
,
_
:=
json
.
Marshal
(
reqBody
)
log
.
Printf
(
"[OAuth] Step 2: Getting authorization code from %s"
,
authURL
)
log
.
Printf
(
"[OAuth] Step 2 Request Body: %s"
,
string
(
reqBodyJSON
))
var
result
struct
{
RedirectURI
string
`json:"redirect_uri"`
}
resp
,
err
:=
client
.
R
()
.
SetContext
(
ctx
)
.
SetCookies
(
&
http
.
Cookie
{
Name
:
"sessionKey"
,
Value
:
sessionKey
,
})
.
SetHeader
(
"Accept"
,
"application/json"
)
.
SetHeader
(
"Accept-Language"
,
"en-US,en;q=0.9"
)
.
SetHeader
(
"Cache-Control"
,
"no-cache"
)
.
SetHeader
(
"Origin"
,
"https://claude.ai"
)
.
SetHeader
(
"Referer"
,
"https://claude.ai/new"
)
.
SetHeader
(
"Content-Type"
,
"application/json"
)
.
SetBody
(
reqBody
)
.
SetSuccessResult
(
&
result
)
.
Post
(
authURL
)
if
err
!=
nil
{
log
.
Printf
(
"[OAuth] Step 2 FAILED - Request error: %v"
,
err
)
return
""
,
fmt
.
Errorf
(
"request failed: %w"
,
err
)
}
log
.
Printf
(
"[OAuth] Step 2 Response - Status: %d, Body: %s"
,
resp
.
StatusCode
,
resp
.
String
())
if
!
resp
.
IsSuccessState
()
{
return
""
,
fmt
.
Errorf
(
"failed to get authorization code: status %d, body: %s"
,
resp
.
StatusCode
,
resp
.
String
())
}
if
result
.
RedirectURI
==
""
{
return
""
,
fmt
.
Errorf
(
"no redirect_uri in response"
)
}
parsedURL
,
err
:=
url
.
Parse
(
result
.
RedirectURI
)
if
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"failed to parse redirect_uri: %w"
,
err
)
}
queryParams
:=
parsedURL
.
Query
()
authCode
:=
queryParams
.
Get
(
"code"
)
responseState
:=
queryParams
.
Get
(
"state"
)
if
authCode
==
""
{
return
""
,
fmt
.
Errorf
(
"no authorization code in redirect_uri"
)
}
fullCode
:=
authCode
if
responseState
!=
""
{
fullCode
=
authCode
+
"#"
+
responseState
}
log
.
Printf
(
"[OAuth] Step 2 SUCCESS - Got authorization code: %s..."
,
authCode
[
:
20
])
return
fullCode
,
nil
}
func
(
s
*
claudeOAuthService
)
ExchangeCodeForToken
(
ctx
context
.
Context
,
code
,
codeVerifier
,
state
,
proxyURL
string
)
(
*
oauth
.
TokenResponse
,
error
)
{
client
:=
createReqClient
(
proxyURL
)
authCode
:=
code
codeState
:=
""
if
len
(
code
)
>
0
{
parts
:=
make
([]
string
,
0
,
2
)
for
i
,
part
:=
range
[]
rune
(
code
)
{
if
part
==
'#'
{
authCode
=
code
[
:
i
]
codeState
=
code
[
i
+
1
:
]
break
}
}
if
len
(
parts
)
==
0
{
authCode
=
code
}
}
reqBody
:=
map
[
string
]
interface
{}{
"code"
:
authCode
,
"grant_type"
:
"authorization_code"
,
"client_id"
:
oauth
.
ClientID
,
"redirect_uri"
:
oauth
.
RedirectURI
,
"code_verifier"
:
codeVerifier
,
}
if
codeState
!=
""
{
reqBody
[
"state"
]
=
codeState
}
reqBodyJSON
,
_
:=
json
.
Marshal
(
reqBody
)
log
.
Printf
(
"[OAuth] Step 3: Exchanging code for token at %s"
,
oauth
.
TokenURL
)
log
.
Printf
(
"[OAuth] Step 3 Request Body: %s"
,
string
(
reqBodyJSON
))
var
tokenResp
oauth
.
TokenResponse
resp
,
err
:=
client
.
R
()
.
SetContext
(
ctx
)
.
SetHeader
(
"Content-Type"
,
"application/json"
)
.
SetBody
(
reqBody
)
.
SetSuccessResult
(
&
tokenResp
)
.
Post
(
oauth
.
TokenURL
)
if
err
!=
nil
{
log
.
Printf
(
"[OAuth] Step 3 FAILED - Request error: %v"
,
err
)
return
nil
,
fmt
.
Errorf
(
"request failed: %w"
,
err
)
}
log
.
Printf
(
"[OAuth] Step 3 Response - Status: %d, Body: %s"
,
resp
.
StatusCode
,
resp
.
String
())
if
!
resp
.
IsSuccessState
()
{
return
nil
,
fmt
.
Errorf
(
"token exchange failed: status %d, body: %s"
,
resp
.
StatusCode
,
resp
.
String
())
}
log
.
Printf
(
"[OAuth] Step 3 SUCCESS - Got access token"
)
return
&
tokenResp
,
nil
}
func
(
s
*
claudeOAuthService
)
RefreshToken
(
ctx
context
.
Context
,
refreshToken
,
proxyURL
string
)
(
*
oauth
.
TokenResponse
,
error
)
{
client
:=
createReqClient
(
proxyURL
)
formData
:=
url
.
Values
{}
formData
.
Set
(
"grant_type"
,
"refresh_token"
)
formData
.
Set
(
"refresh_token"
,
refreshToken
)
formData
.
Set
(
"client_id"
,
oauth
.
ClientID
)
var
tokenResp
oauth
.
TokenResponse
resp
,
err
:=
client
.
R
()
.
SetContext
(
ctx
)
.
SetFormDataFromValues
(
formData
)
.
SetSuccessResult
(
&
tokenResp
)
.
Post
(
oauth
.
TokenURL
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"request failed: %w"
,
err
)
}
if
!
resp
.
IsSuccessState
()
{
return
nil
,
fmt
.
Errorf
(
"token refresh failed: status %d, body: %s"
,
resp
.
StatusCode
,
resp
.
String
())
}
return
&
tokenResp
,
nil
}
func
createReqClient
(
proxyURL
string
)
*
req
.
Client
{
client
:=
req
.
C
()
.
ImpersonateChrome
()
.
SetTimeout
(
60
*
time
.
Second
)
if
proxyURL
!=
""
{
client
.
SetProxyURL
(
proxyURL
)
}
return
client
}
backend/internal/repository/claude_service.go
0 → 100644
View file @
bb500b7b
package
repository
import
(
"net/http"
"net/url"
"time"
"sub2api/internal/config"
"sub2api/internal/service"
)
type
claudeUpstreamService
struct
{
defaultClient
*
http
.
Client
cfg
*
config
.
Config
}
func
NewClaudeUpstream
(
cfg
*
config
.
Config
)
service
.
ClaudeUpstream
{
responseHeaderTimeout
:=
time
.
Duration
(
cfg
.
Gateway
.
ResponseHeaderTimeout
)
*
time
.
Second
if
responseHeaderTimeout
==
0
{
responseHeaderTimeout
=
300
*
time
.
Second
}
transport
:=
&
http
.
Transport
{
MaxIdleConns
:
100
,
MaxIdleConnsPerHost
:
10
,
IdleConnTimeout
:
90
*
time
.
Second
,
ResponseHeaderTimeout
:
responseHeaderTimeout
,
}
return
&
claudeUpstreamService
{
defaultClient
:
&
http
.
Client
{
Transport
:
transport
},
cfg
:
cfg
,
}
}
func
(
s
*
claudeUpstreamService
)
Do
(
req
*
http
.
Request
,
proxyURL
string
)
(
*
http
.
Response
,
error
)
{
if
proxyURL
==
""
{
return
s
.
defaultClient
.
Do
(
req
)
}
client
:=
s
.
createProxyClient
(
proxyURL
)
return
client
.
Do
(
req
)
}
func
(
s
*
claudeUpstreamService
)
createProxyClient
(
proxyURL
string
)
*
http
.
Client
{
parsedURL
,
err
:=
url
.
Parse
(
proxyURL
)
if
err
!=
nil
{
return
s
.
defaultClient
}
responseHeaderTimeout
:=
time
.
Duration
(
s
.
cfg
.
Gateway
.
ResponseHeaderTimeout
)
*
time
.
Second
if
responseHeaderTimeout
==
0
{
responseHeaderTimeout
=
300
*
time
.
Second
}
transport
:=
&
http
.
Transport
{
Proxy
:
http
.
ProxyURL
(
parsedURL
),
MaxIdleConns
:
100
,
MaxIdleConnsPerHost
:
10
,
IdleConnTimeout
:
90
*
time
.
Second
,
ResponseHeaderTimeout
:
responseHeaderTimeout
,
}
return
&
http
.
Client
{
Transport
:
transport
}
}
backend/internal/repository/claude_usage_service.go
0 → 100644
View file @
bb500b7b
package
repository
import
(
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
"sub2api/internal/service"
)
type
claudeUsageService
struct
{}
func
NewClaudeUsageFetcher
()
service
.
ClaudeUsageFetcher
{
return
&
claudeUsageService
{}
}
func
(
s
*
claudeUsageService
)
FetchUsage
(
ctx
context
.
Context
,
accessToken
,
proxyURL
string
)
(
*
service
.
ClaudeUsageResponse
,
error
)
{
transport
:=
http
.
DefaultTransport
.
(
*
http
.
Transport
)
.
Clone
()
if
proxyURL
!=
""
{
if
parsedURL
,
err
:=
url
.
Parse
(
proxyURL
);
err
==
nil
{
transport
.
Proxy
=
http
.
ProxyURL
(
parsedURL
)
}
}
client
:=
&
http
.
Client
{
Transport
:
transport
,
Timeout
:
30
*
time
.
Second
,
}
req
,
err
:=
http
.
NewRequestWithContext
(
ctx
,
"GET"
,
"https://api.anthropic.com/api/oauth/usage"
,
nil
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"create request failed: %w"
,
err
)
}
req
.
Header
.
Set
(
"Authorization"
,
"Bearer "
+
accessToken
)
req
.
Header
.
Set
(
"anthropic-beta"
,
"oauth-2025-04-20"
)
resp
,
err
:=
client
.
Do
(
req
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"request failed: %w"
,
err
)
}
defer
resp
.
Body
.
Close
()
if
resp
.
StatusCode
!=
http
.
StatusOK
{
body
,
_
:=
io
.
ReadAll
(
resp
.
Body
)
return
nil
,
fmt
.
Errorf
(
"API returned status %d: %s"
,
resp
.
StatusCode
,
string
(
body
))
}
var
usageResp
service
.
ClaudeUsageResponse
if
err
:=
json
.
NewDecoder
(
resp
.
Body
)
.
Decode
(
&
usageResp
);
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"decode response failed: %w"
,
err
)
}
return
&
usageResp
,
nil
}
backend/internal/repository/github_release_service.go
0 → 100644
View file @
bb500b7b
package
repository
import
(
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
"sub2api/internal/service"
)
type
githubReleaseClient
struct
{
httpClient
*
http
.
Client
}
func
NewGitHubReleaseClient
()
service
.
GitHubReleaseClient
{
return
&
githubReleaseClient
{
httpClient
:
&
http
.
Client
{
Timeout
:
30
*
time
.
Second
,
},
}
}
func
(
c
*
githubReleaseClient
)
FetchLatestRelease
(
ctx
context
.
Context
,
repo
string
)
(
*
service
.
GitHubRelease
,
error
)
{
url
:=
fmt
.
Sprintf
(
"https://api.github.com/repos/%s/releases/latest"
,
repo
)
req
,
err
:=
http
.
NewRequestWithContext
(
ctx
,
http
.
MethodGet
,
url
,
nil
)
if
err
!=
nil
{
return
nil
,
err
}
req
.
Header
.
Set
(
"Accept"
,
"application/vnd.github.v3+json"
)
req
.
Header
.
Set
(
"User-Agent"
,
"Sub2API-Updater"
)
resp
,
err
:=
c
.
httpClient
.
Do
(
req
)
if
err
!=
nil
{
return
nil
,
err
}
defer
resp
.
Body
.
Close
()
if
resp
.
StatusCode
!=
http
.
StatusOK
{
return
nil
,
fmt
.
Errorf
(
"GitHub API returned %d"
,
resp
.
StatusCode
)
}
var
release
service
.
GitHubRelease
if
err
:=
json
.
NewDecoder
(
resp
.
Body
)
.
Decode
(
&
release
);
err
!=
nil
{
return
nil
,
err
}
return
&
release
,
nil
}
func
(
c
*
githubReleaseClient
)
DownloadFile
(
ctx
context
.
Context
,
url
,
dest
string
,
maxSize
int64
)
error
{
req
,
err
:=
http
.
NewRequestWithContext
(
ctx
,
http
.
MethodGet
,
url
,
nil
)
if
err
!=
nil
{
return
err
}
client
:=
&
http
.
Client
{
Timeout
:
10
*
time
.
Minute
}
resp
,
err
:=
client
.
Do
(
req
)
if
err
!=
nil
{
return
err
}
defer
resp
.
Body
.
Close
()
if
resp
.
StatusCode
!=
http
.
StatusOK
{
return
fmt
.
Errorf
(
"download returned %d"
,
resp
.
StatusCode
)
}
// SECURITY: Check Content-Length if available
if
resp
.
ContentLength
>
maxSize
{
return
fmt
.
Errorf
(
"file too large: %d bytes (max %d)"
,
resp
.
ContentLength
,
maxSize
)
}
out
,
err
:=
os
.
Create
(
dest
)
if
err
!=
nil
{
return
err
}
defer
out
.
Close
()
// SECURITY: Use LimitReader to enforce max download size even if Content-Length is missing/wrong
limited
:=
io
.
LimitReader
(
resp
.
Body
,
maxSize
+
1
)
written
,
err
:=
io
.
Copy
(
out
,
limited
)
if
err
!=
nil
{
return
err
}
// Check if we hit the limit (downloaded more than maxSize)
if
written
>
maxSize
{
os
.
Remove
(
dest
)
// Clean up partial file
return
fmt
.
Errorf
(
"download exceeded maximum size of %d bytes"
,
maxSize
)
}
return
nil
}
func
(
c
*
githubReleaseClient
)
FetchChecksumFile
(
ctx
context
.
Context
,
url
string
)
([]
byte
,
error
)
{
req
,
err
:=
http
.
NewRequestWithContext
(
ctx
,
http
.
MethodGet
,
url
,
nil
)
if
err
!=
nil
{
return
nil
,
err
}
resp
,
err
:=
c
.
httpClient
.
Do
(
req
)
if
err
!=
nil
{
return
nil
,
err
}
defer
resp
.
Body
.
Close
()
if
resp
.
StatusCode
!=
http
.
StatusOK
{
return
nil
,
fmt
.
Errorf
(
"HTTP %d"
,
resp
.
StatusCode
)
}
return
io
.
ReadAll
(
resp
.
Body
)
}
backend/internal/repository/pricing_service.go
0 → 100644
View file @
bb500b7b
package
repository
import
(
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
"sub2api/internal/service"
)
type
pricingRemoteClient
struct
{
httpClient
*
http
.
Client
}
func
NewPricingRemoteClient
()
service
.
PricingRemoteClient
{
return
&
pricingRemoteClient
{
httpClient
:
&
http
.
Client
{
Timeout
:
30
*
time
.
Second
,
},
}
}
func
(
c
*
pricingRemoteClient
)
FetchPricingJSON
(
ctx
context
.
Context
,
url
string
)
([]
byte
,
error
)
{
req
,
err
:=
http
.
NewRequestWithContext
(
ctx
,
http
.
MethodGet
,
url
,
nil
)
if
err
!=
nil
{
return
nil
,
err
}
resp
,
err
:=
c
.
httpClient
.
Do
(
req
)
if
err
!=
nil
{
return
nil
,
err
}
defer
resp
.
Body
.
Close
()
if
resp
.
StatusCode
!=
http
.
StatusOK
{
return
nil
,
fmt
.
Errorf
(
"HTTP %d"
,
resp
.
StatusCode
)
}
return
io
.
ReadAll
(
resp
.
Body
)
}
func
(
c
*
pricingRemoteClient
)
FetchHashText
(
ctx
context
.
Context
,
url
string
)
(
string
,
error
)
{
req
,
err
:=
http
.
NewRequestWithContext
(
ctx
,
http
.
MethodGet
,
url
,
nil
)
if
err
!=
nil
{
return
""
,
err
}
resp
,
err
:=
c
.
httpClient
.
Do
(
req
)
if
err
!=
nil
{
return
""
,
err
}
defer
resp
.
Body
.
Close
()
if
resp
.
StatusCode
!=
http
.
StatusOK
{
return
""
,
fmt
.
Errorf
(
"HTTP %d"
,
resp
.
StatusCode
)
}
body
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
if
err
!=
nil
{
return
""
,
err
}
// 哈希文件格式:hash filename 或者纯 hash
hash
:=
strings
.
TrimSpace
(
string
(
body
))
parts
:=
strings
.
Fields
(
hash
)
if
len
(
parts
)
>
0
{
return
parts
[
0
],
nil
}
return
hash
,
nil
}
backend/internal/repository/proxy_probe_service.go
0 → 100644
View file @
bb500b7b
package
repository
import
(
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"time"
"sub2api/internal/service"
"golang.org/x/net/proxy"
)
type
proxyProbeService
struct
{}
func
NewProxyExitInfoProber
()
service
.
ProxyExitInfoProber
{
return
&
proxyProbeService
{}
}
func
(
s
*
proxyProbeService
)
ProbeProxy
(
ctx
context
.
Context
,
proxyURL
string
)
(
*
service
.
ProxyExitInfo
,
int64
,
error
)
{
transport
,
err
:=
createProxyTransport
(
proxyURL
)
if
err
!=
nil
{
return
nil
,
0
,
fmt
.
Errorf
(
"failed to create proxy transport: %w"
,
err
)
}
client
:=
&
http
.
Client
{
Transport
:
transport
,
Timeout
:
15
*
time
.
Second
,
}
startTime
:=
time
.
Now
()
req
,
err
:=
http
.
NewRequestWithContext
(
ctx
,
"GET"
,
"https://ipinfo.io/json"
,
nil
)
if
err
!=
nil
{
return
nil
,
0
,
fmt
.
Errorf
(
"failed to create request: %w"
,
err
)
}
resp
,
err
:=
client
.
Do
(
req
)
if
err
!=
nil
{
return
nil
,
0
,
fmt
.
Errorf
(
"proxy connection failed: %w"
,
err
)
}
defer
resp
.
Body
.
Close
()
latencyMs
:=
time
.
Since
(
startTime
)
.
Milliseconds
()
if
resp
.
StatusCode
!=
http
.
StatusOK
{
return
nil
,
latencyMs
,
fmt
.
Errorf
(
"request failed with status: %d"
,
resp
.
StatusCode
)
}
var
ipInfo
struct
{
IP
string
`json:"ip"`
City
string
`json:"city"`
Region
string
`json:"region"`
Country
string
`json:"country"`
}
body
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
if
err
!=
nil
{
return
nil
,
latencyMs
,
fmt
.
Errorf
(
"failed to read response: %w"
,
err
)
}
if
err
:=
json
.
Unmarshal
(
body
,
&
ipInfo
);
err
!=
nil
{
return
nil
,
latencyMs
,
fmt
.
Errorf
(
"failed to parse response: %w"
,
err
)
}
return
&
service
.
ProxyExitInfo
{
IP
:
ipInfo
.
IP
,
City
:
ipInfo
.
City
,
Region
:
ipInfo
.
Region
,
Country
:
ipInfo
.
Country
,
},
latencyMs
,
nil
}
func
createProxyTransport
(
proxyURL
string
)
(
*
http
.
Transport
,
error
)
{
parsedURL
,
err
:=
url
.
Parse
(
proxyURL
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"invalid proxy URL: %w"
,
err
)
}
transport
:=
&
http
.
Transport
{
TLSClientConfig
:
&
tls
.
Config
{
InsecureSkipVerify
:
true
},
}
switch
parsedURL
.
Scheme
{
case
"http"
,
"https"
:
transport
.
Proxy
=
http
.
ProxyURL
(
parsedURL
)
case
"socks5"
:
dialer
,
err
:=
proxy
.
FromURL
(
parsedURL
,
proxy
.
Direct
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"failed to create socks5 dialer: %w"
,
err
)
}
transport
.
DialContext
=
func
(
ctx
context
.
Context
,
network
,
addr
string
)
(
net
.
Conn
,
error
)
{
return
dialer
.
Dial
(
network
,
addr
)
}
default
:
return
nil
,
fmt
.
Errorf
(
"unsupported proxy protocol: %s"
,
parsedURL
.
Scheme
)
}
return
transport
,
nil
}
backend/internal/repository/turnstile_service.go
0 → 100644
View file @
bb500b7b
package
repository
import
(
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"sub2api/internal/service"
)
const
turnstileVerifyURL
=
"https://challenges.cloudflare.com/turnstile/v0/siteverify"
type
turnstileVerifier
struct
{
httpClient
*
http
.
Client
}
func
NewTurnstileVerifier
()
service
.
TurnstileVerifier
{
return
&
turnstileVerifier
{
httpClient
:
&
http
.
Client
{
Timeout
:
10
*
time
.
Second
,
},
}
}
func
(
v
*
turnstileVerifier
)
VerifyToken
(
ctx
context
.
Context
,
secretKey
,
token
,
remoteIP
string
)
(
*
service
.
TurnstileVerifyResponse
,
error
)
{
formData
:=
url
.
Values
{}
formData
.
Set
(
"secret"
,
secretKey
)
formData
.
Set
(
"response"
,
token
)
if
remoteIP
!=
""
{
formData
.
Set
(
"remoteip"
,
remoteIP
)
}
req
,
err
:=
http
.
NewRequestWithContext
(
ctx
,
http
.
MethodPost
,
turnstileVerifyURL
,
strings
.
NewReader
(
formData
.
Encode
()))
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"create request: %w"
,
err
)
}
req
.
Header
.
Set
(
"Content-Type"
,
"application/x-www-form-urlencoded"
)
resp
,
err
:=
v
.
httpClient
.
Do
(
req
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"send request: %w"
,
err
)
}
defer
resp
.
Body
.
Close
()
var
result
service
.
TurnstileVerifyResponse
if
err
:=
json
.
NewDecoder
(
resp
.
Body
)
.
Decode
(
&
result
);
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"decode response: %w"
,
err
)
}
return
&
result
,
nil
}
backend/internal/repository/wire.go
View file @
bb500b7b
...
@@ -29,6 +29,15 @@ var ProviderSet = wire.NewSet(
...
@@ -29,6 +29,15 @@ var ProviderSet = wire.NewSet(
NewRedeemCache
,
NewRedeemCache
,
NewUpdateCache
,
NewUpdateCache
,
// HTTP service ports (DI Strategy A: return interface directly)
NewTurnstileVerifier
,
NewPricingRemoteClient
,
NewGitHubReleaseClient
,
NewProxyExitInfoProber
,
NewClaudeUsageFetcher
,
NewClaudeOAuthClient
,
NewClaudeUpstream
,
// Bind concrete repositories to service port interfaces
// Bind concrete repositories to service port interfaces
wire
.
Bind
(
new
(
ports
.
UserRepository
),
new
(
*
UserRepository
)),
wire
.
Bind
(
new
(
ports
.
UserRepository
),
new
(
*
UserRepository
)),
wire
.
Bind
(
new
(
ports
.
ApiKeyRepository
),
new
(
*
ApiKeyRepository
)),
wire
.
Bind
(
new
(
ports
.
ApiKeyRepository
),
new
(
*
ApiKeyRepository
)),
...
...
backend/internal/service/account_test_service.go
View file @
bb500b7b
...
@@ -10,7 +10,6 @@ import (
...
@@ -10,7 +10,6 @@ import (
"io"
"io"
"log"
"log"
"net/http"
"net/http"
"net/url"
"strconv"
"strconv"
"strings"
"strings"
"time"
"time"
...
@@ -37,19 +36,17 @@ type TestEvent struct {
...
@@ -37,19 +36,17 @@ type TestEvent struct {
// AccountTestService handles account testing operations
// AccountTestService handles account testing operations
type
AccountTestService
struct
{
type
AccountTestService
struct
{
accountRepo
ports
.
AccountRepository
accountRepo
ports
.
AccountRepository
oauthService
*
OAuthService
oauthService
*
OAuthService
httpClient
*
http
.
Client
claudeUpstream
ClaudeUpstream
}
}
// NewAccountTestService creates a new AccountTestService
// NewAccountTestService creates a new AccountTestService
func
NewAccountTestService
(
accountRepo
ports
.
AccountRepository
,
oauthService
*
OAuthService
)
*
AccountTestService
{
func
NewAccountTestService
(
accountRepo
ports
.
AccountRepository
,
oauthService
*
OAuthService
,
claudeUpstream
ClaudeUpstream
)
*
AccountTestService
{
return
&
AccountTestService
{
return
&
AccountTestService
{
accountRepo
:
accountRepo
,
accountRepo
:
accountRepo
,
oauthService
:
oauthService
,
oauthService
:
oauthService
,
httpClient
:
&
http
.
Client
{
claudeUpstream
:
claudeUpstream
,
Timeout
:
60
*
time
.
Second
,
},
}
}
}
}
...
@@ -209,23 +206,13 @@ func (s *AccountTestService) TestAccountConnection(c *gin.Context, accountID int
...
@@ -209,23 +206,13 @@ func (s *AccountTestService) TestAccountConnection(c *gin.Context, accountID int
req
.
Header
.
Set
(
"x-api-key"
,
authToken
)
req
.
Header
.
Set
(
"x-api-key"
,
authToken
)
}
}
//
Configure proxy if account has one
//
Get proxy URL
transport
:=
http
.
DefaultTransport
.
(
*
http
.
Transport
)
.
Clone
()
proxyURL
:=
""
if
account
.
ProxyID
!=
nil
&&
account
.
Proxy
!=
nil
{
if
account
.
ProxyID
!=
nil
&&
account
.
Proxy
!=
nil
{
proxyURL
:=
account
.
Proxy
.
URL
()
proxyURL
=
account
.
Proxy
.
URL
()
if
proxyURL
!=
""
{
if
parsedURL
,
err
:=
url
.
Parse
(
proxyURL
);
err
==
nil
{
transport
.
Proxy
=
http
.
ProxyURL
(
parsedURL
)
}
}
}
client
:=
&
http
.
Client
{
Transport
:
transport
,
Timeout
:
60
*
time
.
Second
,
}
}
resp
,
err
:=
client
.
Do
(
req
)
resp
,
err
:=
s
.
claudeUpstream
.
Do
(
req
,
proxyURL
)
if
err
!=
nil
{
if
err
!=
nil
{
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Request failed: %s"
,
err
.
Error
()))
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Request failed: %s"
,
err
.
Error
()))
}
}
...
...
backend/internal/service/account_usage_service.go
View file @
bb500b7b
...
@@ -2,12 +2,8 @@ package service
...
@@ -2,12 +2,8 @@ package service
import
(
import
(
"context"
"context"
"encoding/json"
"fmt"
"fmt"
"io"
"log"
"log"
"net/http"
"net/url"
"sync"
"sync"
"time"
"time"
...
@@ -65,23 +61,26 @@ type ClaudeUsageResponse struct {
...
@@ -65,23 +61,26 @@ type ClaudeUsageResponse struct {
}
`json:"seven_day_sonnet"`
}
`json:"seven_day_sonnet"`
}
}
// ClaudeUsageFetcher fetches usage data from Anthropic OAuth API
type
ClaudeUsageFetcher
interface
{
FetchUsage
(
ctx
context
.
Context
,
accessToken
,
proxyURL
string
)
(
*
ClaudeUsageResponse
,
error
)
}
// AccountUsageService 账号使用量查询服务
// AccountUsageService 账号使用量查询服务
type
AccountUsageService
struct
{
type
AccountUsageService
struct
{
accountRepo
ports
.
AccountRepository
accountRepo
ports
.
AccountRepository
usageLogRepo
ports
.
UsageLogRepository
usageLogRepo
ports
.
UsageLogRepository
oauthService
*
OAuthService
oauthService
*
OAuthService
httpClient
*
http
.
Client
usageFetcher
ClaudeUsageFetcher
}
}
// NewAccountUsageService 创建AccountUsageService实例
// NewAccountUsageService 创建AccountUsageService实例
func
NewAccountUsageService
(
accountRepo
ports
.
AccountRepository
,
usageLogRepo
ports
.
UsageLogRepository
,
oauthService
*
OAuthService
)
*
AccountUsageService
{
func
NewAccountUsageService
(
accountRepo
ports
.
AccountRepository
,
usageLogRepo
ports
.
UsageLogRepository
,
oauthService
*
OAuthService
,
usageFetcher
ClaudeUsageFetcher
)
*
AccountUsageService
{
return
&
AccountUsageService
{
return
&
AccountUsageService
{
accountRepo
:
accountRepo
,
accountRepo
:
accountRepo
,
usageLogRepo
:
usageLogRepo
,
usageLogRepo
:
usageLogRepo
,
oauthService
:
oauthService
,
oauthService
:
oauthService
,
httpClient
:
&
http
.
Client
{
usageFetcher
:
usageFetcher
,
Timeout
:
30
*
time
.
Second
,
},
}
}
}
}
...
@@ -179,58 +178,23 @@ func (s *AccountUsageService) GetTodayStats(ctx context.Context, accountID int64
...
@@ -179,58 +178,23 @@ func (s *AccountUsageService) GetTodayStats(ctx context.Context, accountID int64
// fetchOAuthUsage 从Anthropic API获取OAuth账号的使用量
// fetchOAuthUsage 从Anthropic API获取OAuth账号的使用量
func
(
s
*
AccountUsageService
)
fetchOAuthUsage
(
ctx
context
.
Context
,
account
*
model
.
Account
)
(
*
UsageInfo
,
error
)
{
func
(
s
*
AccountUsageService
)
fetchOAuthUsage
(
ctx
context
.
Context
,
account
*
model
.
Account
)
(
*
UsageInfo
,
error
)
{
// 获取access token(从credentials中获取)
accessToken
:=
account
.
GetCredential
(
"access_token"
)
accessToken
:=
account
.
GetCredential
(
"access_token"
)
if
accessToken
==
""
{
if
accessToken
==
""
{
return
nil
,
fmt
.
Errorf
(
"no access token available"
)
return
nil
,
fmt
.
Errorf
(
"no access token available"
)
}
}
// 获取代理配置
var
proxyURL
string
transport
:=
http
.
DefaultTransport
.
(
*
http
.
Transport
)
.
Clone
()
if
account
.
ProxyID
!=
nil
&&
account
.
Proxy
!=
nil
{
if
account
.
ProxyID
!=
nil
&&
account
.
Proxy
!=
nil
{
proxyURL
:=
account
.
Proxy
.
URL
()
proxyURL
=
account
.
Proxy
.
URL
()
if
proxyURL
!=
""
{
if
parsedURL
,
err
:=
url
.
Parse
(
proxyURL
);
err
==
nil
{
transport
.
Proxy
=
http
.
ProxyURL
(
parsedURL
)
}
}
}
client
:=
&
http
.
Client
{
Transport
:
transport
,
Timeout
:
30
*
time
.
Second
,
}
// 构建请求
req
,
err
:=
http
.
NewRequestWithContext
(
ctx
,
"GET"
,
"https://api.anthropic.com/api/oauth/usage"
,
nil
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"create request failed: %w"
,
err
)
}
}
req
.
Header
.
Set
(
"Authorization"
,
"Bearer "
+
accessToken
)
usageResp
,
err
:=
s
.
usageFetcher
.
FetchUsage
(
ctx
,
accessToken
,
proxyURL
)
req
.
Header
.
Set
(
"anthropic-beta"
,
"oauth-2025-04-20"
)
// 发送请求
resp
,
err
:=
client
.
Do
(
req
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"request failed: %w"
,
err
)
return
nil
,
err
}
defer
resp
.
Body
.
Close
()
if
resp
.
StatusCode
!=
http
.
StatusOK
{
body
,
_
:=
io
.
ReadAll
(
resp
.
Body
)
return
nil
,
fmt
.
Errorf
(
"API returned status %d: %s"
,
resp
.
StatusCode
,
string
(
body
))
}
// 解析响应
var
usageResp
ClaudeUsageResponse
if
err
:=
json
.
NewDecoder
(
resp
.
Body
)
.
Decode
(
&
usageResp
);
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"decode response failed: %w"
,
err
)
}
}
// 转换为UsageInfo
now
:=
time
.
Now
()
now
:=
time
.
Now
()
return
s
.
buildUsageInfo
(
&
usageResp
,
&
now
),
nil
return
s
.
buildUsageInfo
(
usageResp
,
&
now
),
nil
}
}
// parseTime 尝试多种格式解析时间
// parseTime 尝试多种格式解析时间
...
...
backend/internal/service/admin_service.go
View file @
bb500b7b
...
@@ -2,21 +2,14 @@ package service
...
@@ -2,21 +2,14 @@ package service
import
(
import
(
"context"
"context"
"crypto/tls"
"encoding/json"
"errors"
"errors"
"fmt"
"fmt"
"io"
"net"
"net/http"
"net/url"
"time"
"time"
"sub2api/internal/model"
"sub2api/internal/model"
"sub2api/internal/pkg/pagination"
"sub2api/internal/pkg/pagination"
"sub2api/internal/service/ports"
"sub2api/internal/service/ports"
"golang.org/x/net/proxy"
"gorm.io/gorm"
"gorm.io/gorm"
)
)
...
@@ -178,6 +171,19 @@ type ProxyTestResult struct {
...
@@ -178,6 +171,19 @@ type ProxyTestResult struct {
Country
string
`json:"country,omitempty"`
Country
string
`json:"country,omitempty"`
}
}
// ProxyExitInfo represents proxy exit information from ipinfo.io
type
ProxyExitInfo
struct
{
IP
string
City
string
Region
string
Country
string
}
// ProxyExitInfoProber tests proxy connectivity and retrieves exit information
type
ProxyExitInfoProber
interface
{
ProbeProxy
(
ctx
context
.
Context
,
proxyURL
string
)
(
*
ProxyExitInfo
,
int64
,
error
)
}
// adminServiceImpl implements AdminService
// adminServiceImpl implements AdminService
type
adminServiceImpl
struct
{
type
adminServiceImpl
struct
{
userRepo
ports
.
UserRepository
userRepo
ports
.
UserRepository
...
@@ -189,6 +195,7 @@ type adminServiceImpl struct {
...
@@ -189,6 +195,7 @@ type adminServiceImpl struct {
usageLogRepo
ports
.
UsageLogRepository
usageLogRepo
ports
.
UsageLogRepository
userSubRepo
ports
.
UserSubscriptionRepository
userSubRepo
ports
.
UserSubscriptionRepository
billingCacheService
*
BillingCacheService
billingCacheService
*
BillingCacheService
proxyProber
ProxyExitInfoProber
}
}
// NewAdminService creates a new AdminService
// NewAdminService creates a new AdminService
...
@@ -202,6 +209,7 @@ func NewAdminService(
...
@@ -202,6 +209,7 @@ func NewAdminService(
usageLogRepo
ports
.
UsageLogRepository
,
usageLogRepo
ports
.
UsageLogRepository
,
userSubRepo
ports
.
UserSubscriptionRepository
,
userSubRepo
ports
.
UserSubscriptionRepository
,
billingCacheService
*
BillingCacheService
,
billingCacheService
*
BillingCacheService
,
proxyProber
ProxyExitInfoProber
,
)
AdminService
{
)
AdminService
{
return
&
adminServiceImpl
{
return
&
adminServiceImpl
{
userRepo
:
userRepo
,
userRepo
:
userRepo
,
...
@@ -213,6 +221,7 @@ func NewAdminService(
...
@@ -213,6 +221,7 @@ func NewAdminService(
usageLogRepo
:
usageLogRepo
,
usageLogRepo
:
usageLogRepo
,
userSubRepo
:
userSubRepo
,
userSubRepo
:
userSubRepo
,
billingCacheService
:
billingCacheService
,
billingCacheService
:
billingCacheService
,
proxyProber
:
proxyProber
,
}
}
}
}
...
@@ -876,79 +885,12 @@ func (s *adminServiceImpl) TestProxy(ctx context.Context, id int64) (*ProxyTestR
...
@@ -876,79 +885,12 @@ func (s *adminServiceImpl) TestProxy(ctx context.Context, id int64) (*ProxyTestR
return
nil
,
err
return
nil
,
err
}
}
return
testProxyConnection
(
ctx
,
proxy
)
}
// testProxyConnection tests proxy connectivity by requesting ipinfo.io/json
func
testProxyConnection
(
ctx
context
.
Context
,
proxy
*
model
.
Proxy
)
(
*
ProxyTestResult
,
error
)
{
proxyURL
:=
proxy
.
URL
()
proxyURL
:=
proxy
.
URL
()
exitInfo
,
latencyMs
,
err
:=
s
.
proxyProber
.
ProbeProxy
(
ctx
,
proxyURL
)
// Create HTTP client with proxy
transport
,
err
:=
createProxyTransport
(
proxyURL
)
if
err
!=
nil
{
if
err
!=
nil
{
return
&
ProxyTestResult
{
return
&
ProxyTestResult
{
Success
:
false
,
Success
:
false
,
Message
:
fmt
.
Sprintf
(
"Failed to create proxy transport: %v"
,
err
),
Message
:
err
.
Error
(),
},
nil
}
client
:=
&
http
.
Client
{
Transport
:
transport
,
Timeout
:
15
*
time
.
Second
,
}
// Measure latency
startTime
:=
time
.
Now
()
req
,
err
:=
http
.
NewRequestWithContext
(
ctx
,
"GET"
,
"https://ipinfo.io/json"
,
nil
)
if
err
!=
nil
{
return
&
ProxyTestResult
{
Success
:
false
,
Message
:
fmt
.
Sprintf
(
"Failed to create request: %v"
,
err
),
},
nil
}
resp
,
err
:=
client
.
Do
(
req
)
if
err
!=
nil
{
return
&
ProxyTestResult
{
Success
:
false
,
Message
:
fmt
.
Sprintf
(
"Proxy connection failed: %v"
,
err
),
},
nil
}
defer
resp
.
Body
.
Close
()
latencyMs
:=
time
.
Since
(
startTime
)
.
Milliseconds
()
if
resp
.
StatusCode
!=
http
.
StatusOK
{
return
&
ProxyTestResult
{
Success
:
false
,
Message
:
fmt
.
Sprintf
(
"Request failed with status: %d"
,
resp
.
StatusCode
),
LatencyMs
:
latencyMs
,
},
nil
}
// Parse ipinfo.io response
var
ipInfo
struct
{
IP
string
`json:"ip"`
City
string
`json:"city"`
Region
string
`json:"region"`
Country
string
`json:"country"`
}
body
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
if
err
!=
nil
{
return
&
ProxyTestResult
{
Success
:
true
,
Message
:
"Proxy is accessible but failed to read response"
,
LatencyMs
:
latencyMs
,
},
nil
}
if
err
:=
json
.
Unmarshal
(
body
,
&
ipInfo
);
err
!=
nil
{
return
&
ProxyTestResult
{
Success
:
true
,
Message
:
"Proxy is accessible but failed to parse response"
,
LatencyMs
:
latencyMs
,
},
nil
},
nil
}
}
...
@@ -956,38 +898,9 @@ func testProxyConnection(ctx context.Context, proxy *model.Proxy) (*ProxyTestRes
...
@@ -956,38 +898,9 @@ func testProxyConnection(ctx context.Context, proxy *model.Proxy) (*ProxyTestRes
Success
:
true
,
Success
:
true
,
Message
:
"Proxy is accessible"
,
Message
:
"Proxy is accessible"
,
LatencyMs
:
latencyMs
,
LatencyMs
:
latencyMs
,
IPAddress
:
ip
Info
.
IP
,
IPAddress
:
exit
Info
.
IP
,
City
:
ip
Info
.
City
,
City
:
exit
Info
.
City
,
Region
:
ip
Info
.
Region
,
Region
:
exit
Info
.
Region
,
Country
:
ip
Info
.
Country
,
Country
:
exit
Info
.
Country
,
},
nil
},
nil
}
}
// createProxyTransport creates an HTTP transport with the given proxy URL
func
createProxyTransport
(
proxyURL
string
)
(
*
http
.
Transport
,
error
)
{
parsedURL
,
err
:=
url
.
Parse
(
proxyURL
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"invalid proxy URL: %w"
,
err
)
}
transport
:=
&
http
.
Transport
{
TLSClientConfig
:
&
tls
.
Config
{
InsecureSkipVerify
:
true
},
}
switch
parsedURL
.
Scheme
{
case
"http"
,
"https"
:
transport
.
Proxy
=
http
.
ProxyURL
(
parsedURL
)
case
"socks5"
:
dialer
,
err
:=
proxy
.
FromURL
(
parsedURL
,
proxy
.
Direct
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"failed to create socks5 dialer: %w"
,
err
)
}
transport
.
DialContext
=
func
(
ctx
context
.
Context
,
network
,
addr
string
)
(
net
.
Conn
,
error
)
{
return
dialer
.
Dial
(
network
,
addr
)
}
default
:
return
nil
,
fmt
.
Errorf
(
"unsupported proxy protocol: %s"
,
parsedURL
.
Scheme
)
}
return
transport
,
nil
}
backend/internal/service/gateway_service.go
View file @
bb500b7b
...
@@ -12,7 +12,6 @@ import (
...
@@ -12,7 +12,6 @@ import (
"io"
"io"
"log"
"log"
"net/http"
"net/http"
"net/url"
"regexp"
"regexp"
"strconv"
"strconv"
"strings"
"strings"
...
@@ -26,6 +25,11 @@ import (
...
@@ -26,6 +25,11 @@ import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin"
)
)
// ClaudeUpstream handles HTTP requests to Claude API
type
ClaudeUpstream
interface
{
Do
(
req
*
http
.
Request
,
proxyURL
string
)
(
*
http
.
Response
,
error
)
}
const
(
const
(
claudeAPIURL
=
"https://api.anthropic.com/v1/messages?beta=true"
claudeAPIURL
=
"https://api.anthropic.com/v1/messages?beta=true"
claudeAPICountTokensURL
=
"https://api.anthropic.com/v1/messages/count_tokens?beta=true"
claudeAPICountTokensURL
=
"https://api.anthropic.com/v1/messages/count_tokens?beta=true"
...
@@ -87,7 +91,7 @@ type GatewayService struct {
...
@@ -87,7 +91,7 @@ type GatewayService struct {
rateLimitService
*
RateLimitService
rateLimitService
*
RateLimitService
billingCacheService
*
BillingCacheService
billingCacheService
*
BillingCacheService
identityService
*
IdentityService
identityService
*
IdentityService
httpClient
*
http
.
Client
claudeUpstream
ClaudeUpstream
}
}
// NewGatewayService creates a new GatewayService
// NewGatewayService creates a new GatewayService
...
@@ -103,20 +107,8 @@ func NewGatewayService(
...
@@ -103,20 +107,8 @@ func NewGatewayService(
rateLimitService
*
RateLimitService
,
rateLimitService
*
RateLimitService
,
billingCacheService
*
BillingCacheService
,
billingCacheService
*
BillingCacheService
,
identityService
*
IdentityService
,
identityService
*
IdentityService
,
claudeUpstream
ClaudeUpstream
,
)
*
GatewayService
{
)
*
GatewayService
{
// 计算响应头超时时间
responseHeaderTimeout
:=
time
.
Duration
(
cfg
.
Gateway
.
ResponseHeaderTimeout
)
*
time
.
Second
if
responseHeaderTimeout
==
0
{
responseHeaderTimeout
=
300
*
time
.
Second
// 默认5分钟,LLM高负载时可能排队较久
}
transport
:=
&
http
.
Transport
{
MaxIdleConns
:
100
,
MaxIdleConnsPerHost
:
10
,
IdleConnTimeout
:
90
*
time
.
Second
,
ResponseHeaderTimeout
:
responseHeaderTimeout
,
// 等待上游响应头的超时
// 注意:不设置整体 Timeout,让流式响应可以无限时间传输
}
return
&
GatewayService
{
return
&
GatewayService
{
accountRepo
:
accountRepo
,
accountRepo
:
accountRepo
,
usageLogRepo
:
usageLogRepo
,
usageLogRepo
:
usageLogRepo
,
...
@@ -129,11 +121,7 @@ func NewGatewayService(
...
@@ -129,11 +121,7 @@ func NewGatewayService(
rateLimitService
:
rateLimitService
,
rateLimitService
:
rateLimitService
,
billingCacheService
:
billingCacheService
,
billingCacheService
:
billingCacheService
,
identityService
:
identityService
,
identityService
:
identityService
,
httpClient
:
&
http
.
Client
{
claudeUpstream
:
claudeUpstream
,
Transport
:
transport
,
// 不设置 Timeout:流式请求可能持续十几分钟
// 超时控制由 Transport.ResponseHeaderTimeout 负责(只控制等待响应头)
},
}
}
}
}
...
@@ -436,19 +424,19 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *m
...
@@ -436,19 +424,19 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *m
}
}
// 构建上游请求
// 构建上游请求
upstreamRe
sult
,
err
:=
s
.
buildUpstreamRequest
(
ctx
,
c
,
account
,
body
,
token
,
tokenType
)
upstreamRe
q
,
err
:=
s
.
buildUpstreamRequest
(
ctx
,
c
,
account
,
body
,
token
,
tokenType
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
//
选择使用的client:如果有代理则使用独立的client,否则使用共享的httpClient
//
获取代理URL
httpClient
:=
s
.
httpClient
proxyURL
:=
""
if
upstreamResult
.
Client
!=
nil
{
if
account
.
ProxyID
!=
nil
&&
account
.
Proxy
!=
nil
{
httpClient
=
upstreamResult
.
Client
proxyURL
=
account
.
Proxy
.
URL
()
}
}
// 发送请求
// 发送请求
resp
,
err
:=
httpClient
.
Do
(
upstreamRe
sult
.
Request
)
resp
,
err
:=
s
.
claudeUpstream
.
Do
(
upstreamRe
q
,
proxyURL
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"upstream request failed: %w"
,
err
)
return
nil
,
fmt
.
Errorf
(
"upstream request failed: %w"
,
err
)
}
}
...
@@ -461,16 +449,11 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *m
...
@@ -461,16 +449,11 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *m
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"token refresh failed: %w"
,
err
)
return
nil
,
fmt
.
Errorf
(
"token refresh failed: %w"
,
err
)
}
}
upstreamRe
sult
,
err
=
s
.
buildUpstreamRequest
(
ctx
,
c
,
account
,
body
,
token
,
tokenType
)
upstreamRe
q
,
err
=
s
.
buildUpstreamRequest
(
ctx
,
c
,
account
,
body
,
token
,
tokenType
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
// 重试时也需要使用正确的client
resp
,
err
=
s
.
claudeUpstream
.
Do
(
upstreamReq
,
proxyURL
)
httpClient
=
s
.
httpClient
if
upstreamResult
.
Client
!=
nil
{
httpClient
=
upstreamResult
.
Client
}
resp
,
err
=
httpClient
.
Do
(
upstreamResult
.
Request
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"retry request failed: %w"
,
err
)
return
nil
,
fmt
.
Errorf
(
"retry request failed: %w"
,
err
)
}
}
...
@@ -509,13 +492,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *m
...
@@ -509,13 +492,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *m
},
nil
},
nil
}
}
// buildUpstreamRequestResult contains the request and optional custom client for proxy
func
(
s
*
GatewayService
)
buildUpstreamRequest
(
ctx
context
.
Context
,
c
*
gin
.
Context
,
account
*
model
.
Account
,
body
[]
byte
,
token
,
tokenType
string
)
(
*
http
.
Request
,
error
)
{
type
buildUpstreamRequestResult
struct
{
Request
*
http
.
Request
Client
*
http
.
Client
// nil means use default s.httpClient
}
func
(
s
*
GatewayService
)
buildUpstreamRequest
(
ctx
context
.
Context
,
c
*
gin
.
Context
,
account
*
model
.
Account
,
body
[]
byte
,
token
,
tokenType
string
)
(
*
buildUpstreamRequestResult
,
error
)
{
// 确定目标URL
// 确定目标URL
targetURL
:=
claudeAPIURL
targetURL
:=
claudeAPIURL
if
account
.
Type
==
model
.
AccountTypeApiKey
{
if
account
.
Type
==
model
.
AccountTypeApiKey
{
...
@@ -584,36 +561,7 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex
...
@@ -584,36 +561,7 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex
req
.
Header
.
Set
(
"anthropic-beta"
,
s
.
getBetaHeader
(
body
,
c
.
GetHeader
(
"anthropic-beta"
)))
req
.
Header
.
Set
(
"anthropic-beta"
,
s
.
getBetaHeader
(
body
,
c
.
GetHeader
(
"anthropic-beta"
)))
}
}
// 配置代理 - 创建独立的client避免并发修改共享httpClient
return
req
,
nil
var
customClient
*
http
.
Client
if
account
.
ProxyID
!=
nil
&&
account
.
Proxy
!=
nil
{
proxyURL
:=
account
.
Proxy
.
URL
()
if
proxyURL
!=
""
{
if
parsedURL
,
err
:=
url
.
Parse
(
proxyURL
);
err
==
nil
{
// 计算响应头超时时间(与默认 Transport 保持一致)
responseHeaderTimeout
:=
time
.
Duration
(
s
.
cfg
.
Gateway
.
ResponseHeaderTimeout
)
*
time
.
Second
if
responseHeaderTimeout
==
0
{
responseHeaderTimeout
=
300
*
time
.
Second
}
transport
:=
&
http
.
Transport
{
Proxy
:
http
.
ProxyURL
(
parsedURL
),
MaxIdleConns
:
100
,
MaxIdleConnsPerHost
:
10
,
IdleConnTimeout
:
90
*
time
.
Second
,
ResponseHeaderTimeout
:
responseHeaderTimeout
,
}
// 创建独立的client,避免并发时修改共享的s.httpClient.Transport
customClient
=
&
http
.
Client
{
Transport
:
transport
,
}
}
}
}
return
&
buildUpstreamRequestResult
{
Request
:
req
,
Client
:
customClient
,
},
nil
}
}
// getBetaHeader 处理anthropic-beta header
// getBetaHeader 处理anthropic-beta header
...
@@ -1085,20 +1033,20 @@ func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context,
...
@@ -1085,20 +1033,20 @@ func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context,
}
}
// 构建上游请求
// 构建上游请求
upstreamRe
sult
,
err
:=
s
.
buildCountTokensRequest
(
ctx
,
c
,
account
,
body
,
token
,
tokenType
)
upstreamRe
q
,
err
:=
s
.
buildCountTokensRequest
(
ctx
,
c
,
account
,
body
,
token
,
tokenType
)
if
err
!=
nil
{
if
err
!=
nil
{
s
.
countTokensError
(
c
,
http
.
StatusInternalServerError
,
"api_error"
,
"Failed to build request"
)
s
.
countTokensError
(
c
,
http
.
StatusInternalServerError
,
"api_error"
,
"Failed to build request"
)
return
err
return
err
}
}
//
选择 HTTP client
//
获取代理URL
httpClient
:=
s
.
httpClient
proxyURL
:=
""
if
upstreamResult
.
Client
!=
nil
{
if
account
.
ProxyID
!=
nil
&&
account
.
Proxy
!=
nil
{
httpClient
=
upstreamResult
.
Client
proxyURL
=
account
.
Proxy
.
URL
()
}
}
// 发送请求
// 发送请求
resp
,
err
:=
httpClient
.
Do
(
upstreamRe
sult
.
Request
)
resp
,
err
:=
s
.
claudeUpstream
.
Do
(
upstreamRe
q
,
proxyURL
)
if
err
!=
nil
{
if
err
!=
nil
{
s
.
countTokensError
(
c
,
http
.
StatusBadGateway
,
"upstream_error"
,
"Request failed"
)
s
.
countTokensError
(
c
,
http
.
StatusBadGateway
,
"upstream_error"
,
"Request failed"
)
return
fmt
.
Errorf
(
"upstream request failed: %w"
,
err
)
return
fmt
.
Errorf
(
"upstream request failed: %w"
,
err
)
...
@@ -1113,15 +1061,11 @@ func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context,
...
@@ -1113,15 +1061,11 @@ func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context,
s
.
countTokensError
(
c
,
http
.
StatusBadGateway
,
"upstream_error"
,
"Token refresh failed"
)
s
.
countTokensError
(
c
,
http
.
StatusBadGateway
,
"upstream_error"
,
"Token refresh failed"
)
return
fmt
.
Errorf
(
"token refresh failed: %w"
,
err
)
return
fmt
.
Errorf
(
"token refresh failed: %w"
,
err
)
}
}
upstreamRe
sult
,
err
=
s
.
buildCountTokensRequest
(
ctx
,
c
,
account
,
body
,
token
,
tokenType
)
upstreamRe
q
,
err
=
s
.
buildCountTokensRequest
(
ctx
,
c
,
account
,
body
,
token
,
tokenType
)
if
err
!=
nil
{
if
err
!=
nil
{
return
err
return
err
}
}
httpClient
=
s
.
httpClient
resp
,
err
=
s
.
claudeUpstream
.
Do
(
upstreamReq
,
proxyURL
)
if
upstreamResult
.
Client
!=
nil
{
httpClient
=
upstreamResult
.
Client
}
resp
,
err
=
httpClient
.
Do
(
upstreamResult
.
Request
)
if
err
!=
nil
{
if
err
!=
nil
{
s
.
countTokensError
(
c
,
http
.
StatusBadGateway
,
"upstream_error"
,
"Retry failed"
)
s
.
countTokensError
(
c
,
http
.
StatusBadGateway
,
"upstream_error"
,
"Retry failed"
)
return
fmt
.
Errorf
(
"retry request failed: %w"
,
err
)
return
fmt
.
Errorf
(
"retry request failed: %w"
,
err
)
...
@@ -1159,7 +1103,7 @@ func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context,
...
@@ -1159,7 +1103,7 @@ func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context,
}
}
// buildCountTokensRequest 构建 count_tokens 上游请求
// buildCountTokensRequest 构建 count_tokens 上游请求
func
(
s
*
GatewayService
)
buildCountTokensRequest
(
ctx
context
.
Context
,
c
*
gin
.
Context
,
account
*
model
.
Account
,
body
[]
byte
,
token
,
tokenType
string
)
(
*
buildUpstreamRequestResul
t
,
error
)
{
func
(
s
*
GatewayService
)
buildCountTokensRequest
(
ctx
context
.
Context
,
c
*
gin
.
Context
,
account
*
model
.
Account
,
body
[]
byte
,
token
,
tokenType
string
)
(
*
http
.
Reques
t
,
error
)
{
// 确定目标 URL
// 确定目标 URL
targetURL
:=
claudeAPICountTokensURL
targetURL
:=
claudeAPICountTokensURL
if
account
.
Type
==
model
.
AccountTypeApiKey
{
if
account
.
Type
==
model
.
AccountTypeApiKey
{
...
@@ -1223,32 +1167,7 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con
...
@@ -1223,32 +1167,7 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con
req
.
Header
.
Set
(
"anthropic-beta"
,
s
.
getBetaHeader
(
body
,
c
.
GetHeader
(
"anthropic-beta"
)))
req
.
Header
.
Set
(
"anthropic-beta"
,
s
.
getBetaHeader
(
body
,
c
.
GetHeader
(
"anthropic-beta"
)))
}
}
// 配置代理
return
req
,
nil
var
customClient
*
http
.
Client
if
account
.
ProxyID
!=
nil
&&
account
.
Proxy
!=
nil
{
proxyURL
:=
account
.
Proxy
.
URL
()
if
proxyURL
!=
""
{
if
parsedURL
,
err
:=
url
.
Parse
(
proxyURL
);
err
==
nil
{
responseHeaderTimeout
:=
time
.
Duration
(
s
.
cfg
.
Gateway
.
ResponseHeaderTimeout
)
*
time
.
Second
if
responseHeaderTimeout
==
0
{
responseHeaderTimeout
=
300
*
time
.
Second
}
transport
:=
&
http
.
Transport
{
Proxy
:
http
.
ProxyURL
(
parsedURL
),
MaxIdleConns
:
100
,
MaxIdleConnsPerHost
:
10
,
IdleConnTimeout
:
90
*
time
.
Second
,
ResponseHeaderTimeout
:
responseHeaderTimeout
,
}
customClient
=
&
http
.
Client
{
Transport
:
transport
}
}
}
}
return
&
buildUpstreamRequestResult
{
Request
:
req
,
Client
:
customClient
,
},
nil
}
}
// countTokensError 返回 count_tokens 错误响应
// countTokensError 返回 count_tokens 错误响应
...
...
backend/internal/service/oauth_service.go
View file @
bb500b7b
...
@@ -2,32 +2,36 @@ package service
...
@@ -2,32 +2,36 @@ package service
import
(
import
(
"context"
"context"
"encoding/json"
"fmt"
"fmt"
"log"
"log"
"net/http"
"net/url"
"strings"
"time"
"time"
"sub2api/internal/model"
"sub2api/internal/model"
"sub2api/internal/pkg/oauth"
"sub2api/internal/pkg/oauth"
"sub2api/internal/service/ports"
"sub2api/internal/service/ports"
"github.com/imroc/req/v3"
)
)
// ClaudeOAuthClient handles HTTP requests for Claude OAuth flows
type
ClaudeOAuthClient
interface
{
GetOrganizationUUID
(
ctx
context
.
Context
,
sessionKey
,
proxyURL
string
)
(
string
,
error
)
GetAuthorizationCode
(
ctx
context
.
Context
,
sessionKey
,
orgUUID
,
scope
,
codeChallenge
,
state
,
proxyURL
string
)
(
string
,
error
)
ExchangeCodeForToken
(
ctx
context
.
Context
,
code
,
codeVerifier
,
state
,
proxyURL
string
)
(
*
oauth
.
TokenResponse
,
error
)
RefreshToken
(
ctx
context
.
Context
,
refreshToken
,
proxyURL
string
)
(
*
oauth
.
TokenResponse
,
error
)
}
// OAuthService handles OAuth authentication flows
// OAuthService handles OAuth authentication flows
type
OAuthService
struct
{
type
OAuthService
struct
{
sessionStore
*
oauth
.
SessionStore
sessionStore
*
oauth
.
SessionStore
proxyRepo
ports
.
ProxyRepository
proxyRepo
ports
.
ProxyRepository
oauthClient
ClaudeOAuthClient
}
}
// NewOAuthService creates a new OAuth service
// NewOAuthService creates a new OAuth service
func
NewOAuthService
(
proxyRepo
ports
.
ProxyRepository
)
*
OAuthService
{
func
NewOAuthService
(
proxyRepo
ports
.
ProxyRepository
,
oauthClient
ClaudeOAuthClient
)
*
OAuthService
{
return
&
OAuthService
{
return
&
OAuthService
{
sessionStore
:
oauth
.
NewSessionStore
(),
sessionStore
:
oauth
.
NewSessionStore
(),
proxyRepo
:
proxyRepo
,
proxyRepo
:
proxyRepo
,
oauthClient
:
oauthClient
,
}
}
}
}
...
@@ -210,177 +214,21 @@ func (s *OAuthService) CookieAuth(ctx context.Context, input *CookieAuthInput) (
...
@@ -210,177 +214,21 @@ func (s *OAuthService) CookieAuth(ctx context.Context, input *CookieAuthInput) (
// getOrganizationUUID gets the organization UUID from claude.ai using sessionKey
// getOrganizationUUID gets the organization UUID from claude.ai using sessionKey
func
(
s
*
OAuthService
)
getOrganizationUUID
(
ctx
context
.
Context
,
sessionKey
,
proxyURL
string
)
(
string
,
error
)
{
func
(
s
*
OAuthService
)
getOrganizationUUID
(
ctx
context
.
Context
,
sessionKey
,
proxyURL
string
)
(
string
,
error
)
{
client
:=
s
.
createReqClient
(
proxyURL
)
return
s
.
oauthClient
.
GetOrganizationUUID
(
ctx
,
sessionKey
,
proxyURL
)
var
orgs
[]
struct
{
UUID
string
`json:"uuid"`
}
targetURL
:=
"https://claude.ai/api/organizations"
log
.
Printf
(
"[OAuth] Step 1: Getting organization UUID from %s"
,
targetURL
)
resp
,
err
:=
client
.
R
()
.
SetContext
(
ctx
)
.
SetCookies
(
&
http
.
Cookie
{
Name
:
"sessionKey"
,
Value
:
sessionKey
,
})
.
SetSuccessResult
(
&
orgs
)
.
Get
(
targetURL
)
if
err
!=
nil
{
log
.
Printf
(
"[OAuth] Step 1 FAILED - Request error: %v"
,
err
)
return
""
,
fmt
.
Errorf
(
"request failed: %w"
,
err
)
}
log
.
Printf
(
"[OAuth] Step 1 Response - Status: %d, Body: %s"
,
resp
.
StatusCode
,
resp
.
String
())
if
!
resp
.
IsSuccessState
()
{
return
""
,
fmt
.
Errorf
(
"failed to get organizations: status %d, body: %s"
,
resp
.
StatusCode
,
resp
.
String
())
}
if
len
(
orgs
)
==
0
{
return
""
,
fmt
.
Errorf
(
"no organizations found"
)
}
log
.
Printf
(
"[OAuth] Step 1 SUCCESS - Got org UUID: %s"
,
orgs
[
0
]
.
UUID
)
return
orgs
[
0
]
.
UUID
,
nil
}
}
// getAuthorizationCode gets the authorization code using sessionKey
// getAuthorizationCode gets the authorization code using sessionKey
func
(
s
*
OAuthService
)
getAuthorizationCode
(
ctx
context
.
Context
,
sessionKey
,
orgUUID
,
scope
,
codeChallenge
,
state
,
proxyURL
string
)
(
string
,
error
)
{
func
(
s
*
OAuthService
)
getAuthorizationCode
(
ctx
context
.
Context
,
sessionKey
,
orgUUID
,
scope
,
codeChallenge
,
state
,
proxyURL
string
)
(
string
,
error
)
{
client
:=
s
.
createReqClient
(
proxyURL
)
return
s
.
oauthClient
.
GetAuthorizationCode
(
ctx
,
sessionKey
,
orgUUID
,
scope
,
codeChallenge
,
state
,
proxyURL
)
authURL
:=
fmt
.
Sprintf
(
"https://claude.ai/v1/oauth/%s/authorize"
,
orgUUID
)
// Build request body - must include organization_uuid as per CRS
reqBody
:=
map
[
string
]
interface
{}{
"response_type"
:
"code"
,
"client_id"
:
oauth
.
ClientID
,
"organization_uuid"
:
orgUUID
,
// Required field!
"redirect_uri"
:
oauth
.
RedirectURI
,
"scope"
:
scope
,
"state"
:
state
,
"code_challenge"
:
codeChallenge
,
"code_challenge_method"
:
"S256"
,
}
reqBodyJSON
,
_
:=
json
.
Marshal
(
reqBody
)
log
.
Printf
(
"[OAuth] Step 2: Getting authorization code from %s"
,
authURL
)
log
.
Printf
(
"[OAuth] Step 2 Request Body: %s"
,
string
(
reqBodyJSON
))
// Response contains redirect_uri with code, not direct code field
var
result
struct
{
RedirectURI
string
`json:"redirect_uri"`
}
resp
,
err
:=
client
.
R
()
.
SetContext
(
ctx
)
.
SetCookies
(
&
http
.
Cookie
{
Name
:
"sessionKey"
,
Value
:
sessionKey
,
})
.
SetHeader
(
"Accept"
,
"application/json"
)
.
SetHeader
(
"Accept-Language"
,
"en-US,en;q=0.9"
)
.
SetHeader
(
"Cache-Control"
,
"no-cache"
)
.
SetHeader
(
"Origin"
,
"https://claude.ai"
)
.
SetHeader
(
"Referer"
,
"https://claude.ai/new"
)
.
SetHeader
(
"Content-Type"
,
"application/json"
)
.
SetBody
(
reqBody
)
.
SetSuccessResult
(
&
result
)
.
Post
(
authURL
)
if
err
!=
nil
{
log
.
Printf
(
"[OAuth] Step 2 FAILED - Request error: %v"
,
err
)
return
""
,
fmt
.
Errorf
(
"request failed: %w"
,
err
)
}
log
.
Printf
(
"[OAuth] Step 2 Response - Status: %d, Body: %s"
,
resp
.
StatusCode
,
resp
.
String
())
if
!
resp
.
IsSuccessState
()
{
return
""
,
fmt
.
Errorf
(
"failed to get authorization code: status %d, body: %s"
,
resp
.
StatusCode
,
resp
.
String
())
}
if
result
.
RedirectURI
==
""
{
return
""
,
fmt
.
Errorf
(
"no redirect_uri in response"
)
}
// Parse redirect_uri to extract code and state
parsedURL
,
err
:=
url
.
Parse
(
result
.
RedirectURI
)
if
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"failed to parse redirect_uri: %w"
,
err
)
}
queryParams
:=
parsedURL
.
Query
()
authCode
:=
queryParams
.
Get
(
"code"
)
responseState
:=
queryParams
.
Get
(
"state"
)
if
authCode
==
""
{
return
""
,
fmt
.
Errorf
(
"no authorization code in redirect_uri"
)
}
// Combine code with state if present (as CRS does)
fullCode
:=
authCode
if
responseState
!=
""
{
fullCode
=
authCode
+
"#"
+
responseState
}
log
.
Printf
(
"[OAuth] Step 2 SUCCESS - Got authorization code: %s..."
,
authCode
[
:
20
])
return
fullCode
,
nil
}
}
// exchangeCodeForToken exchanges authorization code for tokens
// exchangeCodeForToken exchanges authorization code for tokens
func
(
s
*
OAuthService
)
exchangeCodeForToken
(
ctx
context
.
Context
,
code
,
codeVerifier
,
state
,
proxyURL
string
)
(
*
TokenInfo
,
error
)
{
func
(
s
*
OAuthService
)
exchangeCodeForToken
(
ctx
context
.
Context
,
code
,
codeVerifier
,
state
,
proxyURL
string
)
(
*
TokenInfo
,
error
)
{
client
:=
s
.
createReqClient
(
proxyURL
)
tokenResp
,
err
:=
s
.
oauthClient
.
ExchangeCodeForToken
(
ctx
,
code
,
codeVerifier
,
state
,
proxyURL
)
// Parse code#state format if present
authCode
:=
code
codeState
:=
""
if
parts
:=
strings
.
Split
(
code
,
"#"
);
len
(
parts
)
>
1
{
authCode
=
parts
[
0
]
codeState
=
parts
[
1
]
}
// Build JSON body as CRS does (not form data!)
reqBody
:=
map
[
string
]
interface
{}{
"code"
:
authCode
,
"grant_type"
:
"authorization_code"
,
"client_id"
:
oauth
.
ClientID
,
"redirect_uri"
:
oauth
.
RedirectURI
,
"code_verifier"
:
codeVerifier
,
}
// Add state if present
if
codeState
!=
""
{
reqBody
[
"state"
]
=
codeState
}
reqBodyJSON
,
_
:=
json
.
Marshal
(
reqBody
)
log
.
Printf
(
"[OAuth] Step 3: Exchanging code for token at %s"
,
oauth
.
TokenURL
)
log
.
Printf
(
"[OAuth] Step 3 Request Body: %s"
,
string
(
reqBodyJSON
))
var
tokenResp
oauth
.
TokenResponse
resp
,
err
:=
client
.
R
()
.
SetContext
(
ctx
)
.
SetHeader
(
"Content-Type"
,
"application/json"
)
.
SetBody
(
reqBody
)
.
SetSuccessResult
(
&
tokenResp
)
.
Post
(
oauth
.
TokenURL
)
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"[OAuth] Step 3 FAILED - Request error: %v"
,
err
)
return
nil
,
err
return
nil
,
fmt
.
Errorf
(
"request failed: %w"
,
err
)
}
log
.
Printf
(
"[OAuth] Step 3 Response - Status: %d, Body: %s"
,
resp
.
StatusCode
,
resp
.
String
())
if
!
resp
.
IsSuccessState
()
{
return
nil
,
fmt
.
Errorf
(
"token exchange failed: status %d, body: %s"
,
resp
.
StatusCode
,
resp
.
String
())
}
}
log
.
Printf
(
"[OAuth] Step 3 SUCCESS - Got access token"
)
tokenInfo
:=
&
TokenInfo
{
tokenInfo
:=
&
TokenInfo
{
AccessToken
:
tokenResp
.
AccessToken
,
AccessToken
:
tokenResp
.
AccessToken
,
TokenType
:
tokenResp
.
TokenType
,
TokenType
:
tokenResp
.
TokenType
,
...
@@ -390,7 +238,6 @@ func (s *OAuthService) exchangeCodeForToken(ctx context.Context, code, codeVerif
...
@@ -390,7 +238,6 @@ func (s *OAuthService) exchangeCodeForToken(ctx context.Context, code, codeVerif
Scope
:
tokenResp
.
Scope
,
Scope
:
tokenResp
.
Scope
,
}
}
// Extract org_uuid and account_uuid from response
if
tokenResp
.
Organization
!=
nil
&&
tokenResp
.
Organization
.
UUID
!=
""
{
if
tokenResp
.
Organization
!=
nil
&&
tokenResp
.
Organization
.
UUID
!=
""
{
tokenInfo
.
OrgUUID
=
tokenResp
.
Organization
.
UUID
tokenInfo
.
OrgUUID
=
tokenResp
.
Organization
.
UUID
log
.
Printf
(
"[OAuth] Got org_uuid: %s"
,
tokenInfo
.
OrgUUID
)
log
.
Printf
(
"[OAuth] Got org_uuid: %s"
,
tokenInfo
.
OrgUUID
)
...
@@ -405,27 +252,9 @@ func (s *OAuthService) exchangeCodeForToken(ctx context.Context, code, codeVerif
...
@@ -405,27 +252,9 @@ func (s *OAuthService) exchangeCodeForToken(ctx context.Context, code, codeVerif
// RefreshToken refreshes an OAuth token
// RefreshToken refreshes an OAuth token
func
(
s
*
OAuthService
)
RefreshToken
(
ctx
context
.
Context
,
refreshToken
string
,
proxyURL
string
)
(
*
TokenInfo
,
error
)
{
func
(
s
*
OAuthService
)
RefreshToken
(
ctx
context
.
Context
,
refreshToken
string
,
proxyURL
string
)
(
*
TokenInfo
,
error
)
{
client
:=
s
.
createReqClient
(
proxyURL
)
tokenResp
,
err
:=
s
.
oauthClient
.
RefreshToken
(
ctx
,
refreshToken
,
proxyURL
)
formData
:=
url
.
Values
{}
formData
.
Set
(
"grant_type"
,
"refresh_token"
)
formData
.
Set
(
"refresh_token"
,
refreshToken
)
formData
.
Set
(
"client_id"
,
oauth
.
ClientID
)
var
tokenResp
oauth
.
TokenResponse
resp
,
err
:=
client
.
R
()
.
SetContext
(
ctx
)
.
SetFormDataFromValues
(
formData
)
.
SetSuccessResult
(
&
tokenResp
)
.
Post
(
oauth
.
TokenURL
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"request failed: %w"
,
err
)
return
nil
,
err
}
if
!
resp
.
IsSuccessState
()
{
return
nil
,
fmt
.
Errorf
(
"token refresh failed: status %d, body: %s"
,
resp
.
StatusCode
,
resp
.
String
())
}
}
return
&
TokenInfo
{
return
&
TokenInfo
{
...
@@ -455,17 +284,3 @@ func (s *OAuthService) RefreshAccountToken(ctx context.Context, account *model.A
...
@@ -455,17 +284,3 @@ func (s *OAuthService) RefreshAccountToken(ctx context.Context, account *model.A
return
s
.
RefreshToken
(
ctx
,
refreshToken
,
proxyURL
)
return
s
.
RefreshToken
(
ctx
,
refreshToken
,
proxyURL
)
}
}
// createReqClient creates a req client with Chrome impersonation and optional proxy
func
(
s
*
OAuthService
)
createReqClient
(
proxyURL
string
)
*
req
.
Client
{
client
:=
req
.
C
()
.
ImpersonateChrome
()
.
// Impersonate Chrome browser to bypass Cloudflare
SetTimeout
(
60
*
time
.
Second
)
// Set proxy if specified
if
proxyURL
!=
""
{
client
.
SetProxyURL
(
proxyURL
)
}
return
client
}
backend/internal/service/pricing_service.go
View file @
bb500b7b
package
service
package
service
import
(
import
(
"context"
"crypto/sha256"
"crypto/sha256"
"encoding/hex"
"encoding/hex"
"encoding/json"
"encoding/json"
"fmt"
"fmt"
"io"
"log"
"log"
"net/http"
"os"
"os"
"path/filepath"
"path/filepath"
"strings"
"strings"
...
@@ -20,13 +19,19 @@ import (
...
@@ -20,13 +19,19 @@ import (
// LiteLLMModelPricing LiteLLM价格数据结构
// LiteLLMModelPricing LiteLLM价格数据结构
// 只保留我们需要的字段,使用指针来处理可能缺失的值
// 只保留我们需要的字段,使用指针来处理可能缺失的值
type
LiteLLMModelPricing
struct
{
type
LiteLLMModelPricing
struct
{
InputCostPerToken
float64
`json:"input_cost_per_token"`
InputCostPerToken
float64
`json:"input_cost_per_token"`
OutputCostPerToken
float64
`json:"output_cost_per_token"`
OutputCostPerToken
float64
`json:"output_cost_per_token"`
CacheCreationInputTokenCost
float64
`json:"cache_creation_input_token_cost"`
CacheCreationInputTokenCost
float64
`json:"cache_creation_input_token_cost"`
CacheReadInputTokenCost
float64
`json:"cache_read_input_token_cost"`
CacheReadInputTokenCost
float64
`json:"cache_read_input_token_cost"`
LiteLLMProvider
string
`json:"litellm_provider"`
LiteLLMProvider
string
`json:"litellm_provider"`
Mode
string
`json:"mode"`
Mode
string
`json:"mode"`
SupportsPromptCaching
bool
`json:"supports_prompt_caching"`
SupportsPromptCaching
bool
`json:"supports_prompt_caching"`
}
// PricingRemoteClient 远程价格数据获取接口
type
PricingRemoteClient
interface
{
FetchPricingJSON
(
ctx
context
.
Context
,
url
string
)
([]
byte
,
error
)
FetchHashText
(
ctx
context
.
Context
,
url
string
)
(
string
,
error
)
}
}
// LiteLLMRawEntry 用于解析原始JSON数据
// LiteLLMRawEntry 用于解析原始JSON数据
...
@@ -42,11 +47,12 @@ type LiteLLMRawEntry struct {
...
@@ -42,11 +47,12 @@ type LiteLLMRawEntry struct {
// PricingService 动态价格服务
// PricingService 动态价格服务
type
PricingService
struct
{
type
PricingService
struct
{
cfg
*
config
.
Config
cfg
*
config
.
Config
mu
sync
.
RWMutex
remoteClient
PricingRemoteClient
pricingData
map
[
string
]
*
LiteLLMModelPricing
mu
sync
.
RWMutex
lastUpdated
time
.
Time
pricingData
map
[
string
]
*
LiteLLMModelPricing
localHash
string
lastUpdated
time
.
Time
localHash
string
// 停止信号
// 停止信号
stopCh
chan
struct
{}
stopCh
chan
struct
{}
...
@@ -54,11 +60,12 @@ type PricingService struct {
...
@@ -54,11 +60,12 @@ type PricingService struct {
}
}
// NewPricingService 创建价格服务
// NewPricingService 创建价格服务
func
NewPricingService
(
cfg
*
config
.
Config
)
*
PricingService
{
func
NewPricingService
(
cfg
*
config
.
Config
,
remoteClient
PricingRemoteClient
)
*
PricingService
{
s
:=
&
PricingService
{
s
:=
&
PricingService
{
cfg
:
cfg
,
cfg
:
cfg
,
pricingData
:
make
(
map
[
string
]
*
LiteLLMModelPricing
),
remoteClient
:
remoteClient
,
stopCh
:
make
(
chan
struct
{}),
pricingData
:
make
(
map
[
string
]
*
LiteLLMModelPricing
),
stopCh
:
make
(
chan
struct
{}),
}
}
return
s
return
s
}
}
...
@@ -199,20 +206,12 @@ func (s *PricingService) syncWithRemote() error {
...
@@ -199,20 +206,12 @@ func (s *PricingService) syncWithRemote() error {
func
(
s
*
PricingService
)
downloadPricingData
()
error
{
func
(
s
*
PricingService
)
downloadPricingData
()
error
{
log
.
Printf
(
"[Pricing] Downloading from %s"
,
s
.
cfg
.
Pricing
.
RemoteURL
)
log
.
Printf
(
"[Pricing] Downloading from %s"
,
s
.
cfg
.
Pricing
.
RemoteURL
)
client
:=
&
http
.
Client
{
Timeout
:
30
*
time
.
Second
}
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
30
*
time
.
Second
)
resp
,
err
:=
client
.
Get
(
s
.
cfg
.
Pricing
.
RemoteURL
)
defer
cancel
()
if
err
!=
nil
{
return
fmt
.
Errorf
(
"download failed: %w"
,
err
)
}
defer
resp
.
Body
.
Close
()
if
resp
.
StatusCode
!=
http
.
StatusOK
{
body
,
err
:=
s
.
remoteClient
.
FetchPricingJSON
(
ctx
,
s
.
cfg
.
Pricing
.
RemoteURL
)
return
fmt
.
Errorf
(
"download failed: HTTP %d"
,
resp
.
StatusCode
)
}
body
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
if
err
!=
nil
{
if
err
!=
nil
{
return
fmt
.
Errorf
(
"
read response
failed: %w"
,
err
)
return
fmt
.
Errorf
(
"
download
failed: %w"
,
err
)
}
}
// 解析JSON数据(使用灵活的解析方式)
// 解析JSON数据(使用灵活的解析方式)
...
@@ -367,29 +366,10 @@ func (s *PricingService) useFallbackPricing() error {
...
@@ -367,29 +366,10 @@ func (s *PricingService) useFallbackPricing() error {
// fetchRemoteHash 从远程获取哈希值
// fetchRemoteHash 从远程获取哈希值
func
(
s
*
PricingService
)
fetchRemoteHash
()
(
string
,
error
)
{
func
(
s
*
PricingService
)
fetchRemoteHash
()
(
string
,
error
)
{
client
:=
&
http
.
Client
{
Timeout
:
10
*
time
.
Second
}
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
10
*
time
.
Second
)
resp
,
err
:=
client
.
Get
(
s
.
cfg
.
Pricing
.
HashURL
)
defer
cancel
()
if
err
!=
nil
{
return
""
,
err
}
defer
resp
.
Body
.
Close
()
if
resp
.
StatusCode
!=
http
.
StatusOK
{
return
s
.
remoteClient
.
FetchHashText
(
ctx
,
s
.
cfg
.
Pricing
.
HashURL
)
return
""
,
fmt
.
Errorf
(
"HTTP %d"
,
resp
.
StatusCode
)
}
body
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
if
err
!=
nil
{
return
""
,
err
}
// 哈希文件格式:hash filename 或者纯 hash
hash
:=
strings
.
TrimSpace
(
string
(
body
))
parts
:=
strings
.
Fields
(
hash
)
if
len
(
parts
)
>
0
{
return
parts
[
0
],
nil
}
return
hash
,
nil
}
}
// computeFileHash 计算文件哈希
// computeFileHash 计算文件哈希
...
@@ -466,14 +446,14 @@ func (s *PricingService) extractBaseName(model string) string {
...
@@ -466,14 +446,14 @@ func (s *PricingService) extractBaseName(model string) string {
func
(
s
*
PricingService
)
matchByModelFamily
(
model
string
)
*
LiteLLMModelPricing
{
func
(
s
*
PricingService
)
matchByModelFamily
(
model
string
)
*
LiteLLMModelPricing
{
// Claude模型系列匹配规则
// Claude模型系列匹配规则
familyPatterns
:=
map
[
string
][]
string
{
familyPatterns
:=
map
[
string
][]
string
{
"opus-4.5"
:
{
"claude-opus-4.5"
,
"claude-opus-4-5"
},
"opus-4.5"
:
{
"claude-opus-4.5"
,
"claude-opus-4-5"
},
"opus-4"
:
{
"claude-opus-4"
,
"claude-3-opus"
},
"opus-4"
:
{
"claude-opus-4"
,
"claude-3-opus"
},
"sonnet-4.5"
:
{
"claude-sonnet-4.5"
,
"claude-sonnet-4-5"
},
"sonnet-4.5"
:
{
"claude-sonnet-4.5"
,
"claude-sonnet-4-5"
},
"sonnet-4"
:
{
"claude-sonnet-4"
,
"claude-3-5-sonnet"
},
"sonnet-4"
:
{
"claude-sonnet-4"
,
"claude-3-5-sonnet"
},
"sonnet-3.5"
:
{
"claude-3-5-sonnet"
,
"claude-3.5-sonnet"
},
"sonnet-3.5"
:
{
"claude-3-5-sonnet"
,
"claude-3.5-sonnet"
},
"sonnet-3"
:
{
"claude-3-sonnet"
},
"sonnet-3"
:
{
"claude-3-sonnet"
},
"haiku-3.5"
:
{
"claude-3-5-haiku"
,
"claude-3.5-haiku"
},
"haiku-3.5"
:
{
"claude-3-5-haiku"
,
"claude-3.5-haiku"
},
"haiku-3"
:
{
"claude-3-haiku"
},
"haiku-3"
:
{
"claude-3-haiku"
},
}
}
// 确定模型属于哪个系列
// 确定模型属于哪个系列
...
...
backend/internal/service/service.go
View file @
bb500b7b
...
@@ -26,4 +26,5 @@ type Services struct {
...
@@ -26,4 +26,5 @@ type Services struct {
Subscription
*
SubscriptionService
Subscription
*
SubscriptionService
Concurrency
*
ConcurrencyService
Concurrency
*
ConcurrencyService
Identity
*
IdentityService
Identity
*
IdentityService
Update
*
UpdateService
}
}
backend/internal/service/turnstile_service.go
View file @
bb500b7b
...
@@ -2,14 +2,9 @@ package service
...
@@ -2,14 +2,9 @@ package service
import
(
import
(
"context"
"context"
"encoding/json"
"errors"
"errors"
"fmt"
"fmt"
"log"
"log"
"net/http"
"net/url"
"strings"
"time"
)
)
var
(
var
(
...
@@ -19,10 +14,15 @@ var (
...
@@ -19,10 +14,15 @@ var (
const
turnstileVerifyURL
=
"https://challenges.cloudflare.com/turnstile/v0/siteverify"
const
turnstileVerifyURL
=
"https://challenges.cloudflare.com/turnstile/v0/siteverify"
// TurnstileVerifier 验证 Turnstile token 的接口
type
TurnstileVerifier
interface
{
VerifyToken
(
ctx
context
.
Context
,
secretKey
,
token
,
remoteIP
string
)
(
*
TurnstileVerifyResponse
,
error
)
}
// TurnstileService Turnstile 验证服务
// TurnstileService Turnstile 验证服务
type
TurnstileService
struct
{
type
TurnstileService
struct
{
settingService
*
SettingService
settingService
*
SettingService
httpClient
*
http
.
Client
verifier
TurnstileVerifier
}
}
// TurnstileVerifyResponse Cloudflare Turnstile 验证响应
// TurnstileVerifyResponse Cloudflare Turnstile 验证响应
...
@@ -36,12 +36,10 @@ type TurnstileVerifyResponse struct {
...
@@ -36,12 +36,10 @@ type TurnstileVerifyResponse struct {
}
}
// NewTurnstileService 创建 Turnstile 服务实例
// NewTurnstileService 创建 Turnstile 服务实例
func
NewTurnstileService
(
settingService
*
SettingService
)
*
TurnstileService
{
func
NewTurnstileService
(
settingService
*
SettingService
,
verifier
TurnstileVerifier
)
*
TurnstileService
{
return
&
TurnstileService
{
return
&
TurnstileService
{
settingService
:
settingService
,
settingService
:
settingService
,
httpClient
:
&
http
.
Client
{
verifier
:
verifier
,
Timeout
:
10
*
time
.
Second
,
},
}
}
}
}
...
@@ -66,35 +64,12 @@ func (s *TurnstileService) VerifyToken(ctx context.Context, token string, remote
...
@@ -66,35 +64,12 @@ func (s *TurnstileService) VerifyToken(ctx context.Context, token string, remote
return
ErrTurnstileVerificationFailed
return
ErrTurnstileVerificationFailed
}
}
// 构建请求
formData
:=
url
.
Values
{}
formData
.
Set
(
"secret"
,
secretKey
)
formData
.
Set
(
"response"
,
token
)
if
remoteIP
!=
""
{
formData
.
Set
(
"remoteip"
,
remoteIP
)
}
req
,
err
:=
http
.
NewRequestWithContext
(
ctx
,
http
.
MethodPost
,
turnstileVerifyURL
,
strings
.
NewReader
(
formData
.
Encode
()))
if
err
!=
nil
{
return
fmt
.
Errorf
(
"create request: %w"
,
err
)
}
req
.
Header
.
Set
(
"Content-Type"
,
"application/x-www-form-urlencoded"
)
// 发送请求
log
.
Printf
(
"[Turnstile] Verifying token for IP: %s"
,
remoteIP
)
log
.
Printf
(
"[Turnstile] Verifying token for IP: %s"
,
remoteIP
)
res
p
,
err
:=
s
.
httpClient
.
Do
(
req
)
res
ult
,
err
:=
s
.
verifier
.
VerifyToken
(
ctx
,
secretKey
,
token
,
remoteIP
)
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"[Turnstile] Request failed: %v"
,
err
)
log
.
Printf
(
"[Turnstile] Request failed: %v"
,
err
)
return
fmt
.
Errorf
(
"send request: %w"
,
err
)
return
fmt
.
Errorf
(
"send request: %w"
,
err
)
}
}
defer
resp
.
Body
.
Close
()
// 解析响应
var
result
TurnstileVerifyResponse
if
err
:=
json
.
NewDecoder
(
resp
.
Body
)
.
Decode
(
&
result
);
err
!=
nil
{
log
.
Printf
(
"[Turnstile] Failed to decode response: %v"
,
err
)
return
fmt
.
Errorf
(
"decode response: %w"
,
err
)
}
if
!
result
.
Success
{
if
!
result
.
Success
{
log
.
Printf
(
"[Turnstile] Verification failed, error codes: %v"
,
result
.
ErrorCodes
)
log
.
Printf
(
"[Turnstile] Verification failed, error codes: %v"
,
result
.
ErrorCodes
)
...
...
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment