新增功能: - 新增 TLS 指纹 Profile CRUD 管理(Ent schema + 迁移 + Admin API + 前端管理界面) - 支持账号绑定数据库中的自定义 TLS Profile,或随机选择(profile_id=-1) - HTTPUpstream.DoWithTLS 接口从 bool 改为 *tlsfingerprint.Profile,支持按账号指定 Profile - AccountUsageService 注入 TLSFingerprintProfileService,统一 usage 场景与网关的 Profile 解析逻辑 代码优化: - 删除已被 TLSFingerprintProfileService 完全取代的 registry.go 死代码(418 行) - 提取 3 个 dialer 的重复 TLS 握手逻辑为 performTLSHandshake() 共用函数 - 修复 GetTLSFingerprintProfileID 缺少 json.Number 处理的 bug - gateway_service.Forward 中 ResolveTLSProfile 从重试循环内重复调用改为预解析局部变量 - 删除冗余的 buildClientHelloSpec() 单行 wrapper 和 int64(e.ID) 无效转换 - tls_fingerprint_profile_cache.go 日志从 log.Printf 改为 slog 结构化日志 - dialer_capture_test.go 添加 //go:build integration 标签,防止 CI 失败 - 去重 TestProfileExpectation 类型至共享 test_types_test.go - 修复 9 个测试文件缺少 tlsfingerprint import 的编译错误 - 修复 error_policy_integration_test.go 中 handleError 回调签名被错误替换的问题
109 lines
3.5 KiB
Go
109 lines
3.5 KiB
Go
package repository
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"time"
|
||
|
||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||
"github.com/Wei-Shaw/sub2api/internal/pkg/httpclient"
|
||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||
)
|
||
|
||
const defaultClaudeUsageURL = "https://api.anthropic.com/api/oauth/usage"
|
||
|
||
// 默认 User-Agent,与用户抓包的请求一致
|
||
const defaultUsageUserAgent = "claude-code/2.1.7"
|
||
|
||
type claudeUsageService struct {
|
||
usageURL string
|
||
allowPrivateHosts bool
|
||
httpUpstream service.HTTPUpstream
|
||
}
|
||
|
||
// NewClaudeUsageFetcher 创建 Claude 用量获取服务
|
||
// httpUpstream: 可选,如果提供则支持 TLS 指纹伪装
|
||
func NewClaudeUsageFetcher(httpUpstream service.HTTPUpstream) service.ClaudeUsageFetcher {
|
||
return &claudeUsageService{
|
||
usageURL: defaultClaudeUsageURL,
|
||
httpUpstream: httpUpstream,
|
||
}
|
||
}
|
||
|
||
// FetchUsage 简单版本,不支持 TLS 指纹(向后兼容)
|
||
func (s *claudeUsageService) FetchUsage(ctx context.Context, accessToken, proxyURL string) (*service.ClaudeUsageResponse, error) {
|
||
return s.FetchUsageWithOptions(ctx, &service.ClaudeUsageFetchOptions{
|
||
AccessToken: accessToken,
|
||
ProxyURL: proxyURL,
|
||
})
|
||
}
|
||
|
||
// FetchUsageWithOptions 完整版本,支持 TLS 指纹和自定义 User-Agent
|
||
func (s *claudeUsageService) FetchUsageWithOptions(ctx context.Context, opts *service.ClaudeUsageFetchOptions) (*service.ClaudeUsageResponse, error) {
|
||
if opts == nil {
|
||
return nil, fmt.Errorf("options is nil")
|
||
}
|
||
|
||
// 创建请求
|
||
req, err := http.NewRequestWithContext(ctx, "GET", s.usageURL, nil)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("create request failed: %w", err)
|
||
}
|
||
|
||
// 设置请求头(与抓包一致,但不设置 Accept-Encoding,让 Go 自动处理压缩)
|
||
req.Header.Set("Accept", "application/json, text/plain, */*")
|
||
req.Header.Set("Content-Type", "application/json")
|
||
req.Header.Set("Authorization", "Bearer "+opts.AccessToken)
|
||
req.Header.Set("anthropic-beta", "oauth-2025-04-20")
|
||
|
||
// 设置 User-Agent(优先使用缓存的 Fingerprint,否则使用默认值)
|
||
userAgent := defaultUsageUserAgent
|
||
if opts.Fingerprint != nil && opts.Fingerprint.UserAgent != "" {
|
||
userAgent = opts.Fingerprint.UserAgent
|
||
}
|
||
req.Header.Set("User-Agent", userAgent)
|
||
|
||
var resp *http.Response
|
||
|
||
// 如果有 TLS Profile 且有 HTTPUpstream,使用 DoWithTLS
|
||
if opts.TLSProfile != nil && s.httpUpstream != nil {
|
||
resp, err = s.httpUpstream.DoWithTLS(req, opts.ProxyURL, opts.AccountID, 0, opts.TLSProfile)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("request with TLS fingerprint failed: %w", err)
|
||
}
|
||
} else {
|
||
// 不启用 TLS 指纹,使用普通 HTTP 客户端
|
||
client, err := httpclient.GetClient(httpclient.Options{
|
||
ProxyURL: opts.ProxyURL,
|
||
Timeout: 30 * time.Second,
|
||
ValidateResolvedIP: true,
|
||
AllowPrivateHosts: s.allowPrivateHosts,
|
||
})
|
||
if err != nil {
|
||
return nil, fmt.Errorf("create http client failed: %w", err)
|
||
}
|
||
|
||
resp, err = client.Do(req)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("request failed: %w", err)
|
||
}
|
||
}
|
||
defer func() { _ = resp.Body.Close() }()
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
msg := fmt.Sprintf("API returned status %d: %s", resp.StatusCode, string(body))
|
||
return nil, infraerrors.New(http.StatusInternalServerError, "UPSTREAM_ERROR", msg)
|
||
}
|
||
|
||
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
|
||
}
|