- windsurf_gateway_service: 添加上游延迟/TTFT/错误上下文记录 - endpoint: DeriveUpstreamEndpoint 添加 PlatformWindsurf 分支 - ops_error_logger: guessPlatformFromPath 添加 /windsurf/ 识别
218 lines
6.6 KiB
Go
218 lines
6.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
"github.com/Wei-Shaw/sub2api/internal/domain"
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/windsurf"
|
|
)
|
|
|
|
type WindsurfProbeService struct {
|
|
cfg config.WindsurfConfig
|
|
accountRepo AccountRepository
|
|
proxyRepo ProxyRepository
|
|
}
|
|
|
|
func NewWindsurfProbeService(
|
|
cfg config.WindsurfConfig,
|
|
accountRepo AccountRepository,
|
|
proxyRepo ProxyRepository,
|
|
) *WindsurfProbeService {
|
|
return &WindsurfProbeService{
|
|
cfg: cfg,
|
|
accountRepo: accountRepo,
|
|
proxyRepo: proxyRepo,
|
|
}
|
|
}
|
|
|
|
type WindsurfProbeResult struct {
|
|
AccountID int64
|
|
Tier string
|
|
Profile WindsurfProfileSnapshot
|
|
Status WindsurfUserStatusSnapshot
|
|
Error string
|
|
}
|
|
|
|
func (s *WindsurfProbeService) ProbeAccount(ctx context.Context, accountID int64) (*WindsurfProbeResult, error) {
|
|
account, err := s.accountRepo.GetByID(ctx, accountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get account: %w", err)
|
|
}
|
|
if account.Platform != domain.PlatformWindsurf {
|
|
return nil, fmt.Errorf("account %d is not a windsurf account", accountID)
|
|
}
|
|
|
|
creds := LoadWindsurfCredentials(account.Credentials)
|
|
if creds.APIKey == "" {
|
|
return nil, fmt.Errorf("account %d has no api_key", accountID)
|
|
}
|
|
|
|
proxyURL := ""
|
|
if account.ProxyID != nil {
|
|
proxy, err := s.proxyRepo.GetByID(ctx, *account.ProxyID)
|
|
if err == nil {
|
|
proxyURL = proxy.URL()
|
|
}
|
|
}
|
|
|
|
baseURL := s.cfg.UserStatusBaseURL
|
|
if baseURL == "" {
|
|
baseURL = "https://server.codeium.com"
|
|
}
|
|
client, err := windsurf.NewClient(baseURL, proxyURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create client: %w", err)
|
|
}
|
|
|
|
userStatus, err := client.GetUserStatus(ctx, creds.APIKey)
|
|
if err != nil {
|
|
extra := LoadWindsurfExtra(account.Extra)
|
|
extra.Probe.LastProbeAt = time.Now().Format(time.RFC3339)
|
|
extra.Probe.LastProbeError = err.Error()
|
|
account.Extra = StoreWindsurfExtra(extra)
|
|
_ = s.accountRepo.Update(ctx, account)
|
|
return &WindsurfProbeResult{
|
|
AccountID: accountID,
|
|
Error: err.Error(),
|
|
}, nil
|
|
}
|
|
|
|
extra := LoadWindsurfExtra(account.Extra)
|
|
extra.Profile.UserID = userStatus.UserID
|
|
extra.Profile.TeamID = userStatus.TeamID
|
|
extra.Profile.Email = userStatus.Email
|
|
extra.Profile.DisplayName = userStatus.Name
|
|
extra.Profile.PlanName = userStatus.PlanName
|
|
extra.Profile.TierSource = "probe"
|
|
extra.Probe.LastProbeAt = time.Now().Format(time.RFC3339)
|
|
extra.Probe.LastProbeError = ""
|
|
|
|
extra.Quota.LastCheckedAt = time.Now().Format(time.RFC3339)
|
|
extra.Quota.LastError = ""
|
|
extra.Quota.DailyPercent = userStatus.DailyPercent
|
|
extra.Quota.WeeklyPercent = userStatus.WeeklyPercent
|
|
extra.Quota.PromptLimit = userStatus.MonthlyPromptCredits
|
|
extra.Quota.PromptUsed = userStatus.UsedPromptCredits
|
|
extra.Quota.FlexLimit = userStatus.MonthlyFlexCredits
|
|
extra.Quota.FlexUsed = userStatus.UsedFlexCredits
|
|
|
|
if userStatus.MonthlyPromptCredits != nil && *userStatus.MonthlyPromptCredits > 0 {
|
|
used := float64(0)
|
|
if userStatus.UsedPromptCredits != nil {
|
|
used = *userStatus.UsedPromptCredits
|
|
}
|
|
pct := (used / *userStatus.MonthlyPromptCredits) * 100
|
|
extra.UserStatus.MonthlyPromptCredits = int64(*userStatus.MonthlyPromptCredits)
|
|
extra.UserStatus.UserUsedPromptCredits = int64(used)
|
|
if extra.Quota.DailyPercent == nil {
|
|
extra.Quota.DailyPercent = &pct
|
|
}
|
|
}
|
|
extra.UserStatus.LastFetchedAt = time.Now().Format(time.RFC3339)
|
|
|
|
account.Extra = StoreWindsurfExtra(extra)
|
|
if err := s.accountRepo.Update(ctx, account); err != nil {
|
|
slog.Warn("windsurf_probe_save_failed", "account_id", accountID, "error", err)
|
|
}
|
|
|
|
return &WindsurfProbeResult{
|
|
AccountID: accountID,
|
|
Tier: creds.Tier,
|
|
Profile: extra.Profile,
|
|
Status: extra.UserStatus,
|
|
}, nil
|
|
}
|
|
|
|
func (s *WindsurfProbeService) ProbeModelCatalog(ctx context.Context, accountID int64) ([]windsurf.ModelInfo, error) {
|
|
account, err := s.accountRepo.GetByID(ctx, accountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get account: %w", err)
|
|
}
|
|
|
|
creds := LoadWindsurfCredentials(account.Credentials)
|
|
if creds.APIKey == "" {
|
|
return nil, fmt.Errorf("account %d has no api_key", accountID)
|
|
}
|
|
|
|
proxyURL := ""
|
|
if account.ProxyID != nil {
|
|
proxy, err := s.proxyRepo.GetByID(ctx, *account.ProxyID)
|
|
if err == nil {
|
|
proxyURL = proxy.URL()
|
|
}
|
|
}
|
|
|
|
baseURL := s.cfg.UserStatusBaseURL
|
|
if baseURL == "" {
|
|
baseURL = "https://server.codeium.com"
|
|
}
|
|
client, err := windsurf.NewClient(baseURL, proxyURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create client: %w", err)
|
|
}
|
|
|
|
models, err := client.ListModels(ctx, creds.APIKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list models: %w", err)
|
|
}
|
|
|
|
extra := LoadWindsurfExtra(account.Extra)
|
|
extra.Probe.ModelCatalogAt = time.Now().Format(time.RFC3339)
|
|
account.Extra = StoreWindsurfExtra(extra)
|
|
_ = s.accountRepo.Update(ctx, account)
|
|
|
|
return models, nil
|
|
}
|
|
|
|
func (s *WindsurfProbeService) GetRuntime(ctx context.Context, accountID int64) (*WindsurfRuntimeInfo, error) {
|
|
account, err := s.accountRepo.GetByID(ctx, accountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get account: %w", err)
|
|
}
|
|
if account.Platform != domain.PlatformWindsurf {
|
|
return nil, fmt.Errorf("account %d is not a windsurf account", accountID)
|
|
}
|
|
|
|
creds := LoadWindsurfCredentials(account.Credentials)
|
|
extra := LoadWindsurfExtra(account.Extra)
|
|
|
|
info := &WindsurfRuntimeInfo{
|
|
AccountID: accountID,
|
|
Tier: creds.Tier,
|
|
Capabilities: extra.Capabilities,
|
|
ModelMatrix: extra.ModelMatrix,
|
|
}
|
|
|
|
if extra.Probe.LastProbeAt != "" {
|
|
info.LastProbeAt = &extra.Probe.LastProbeAt
|
|
}
|
|
if extra.Refresh.LastStatusRefreshAt != "" {
|
|
info.LastStatusRefreshAt = &extra.Refresh.LastStatusRefreshAt
|
|
}
|
|
if extra.Quota.DailyPercent != nil {
|
|
info.UsagePercent = extra.Quota.DailyPercent
|
|
}
|
|
if extra.UserStatus.MonthlyPromptCredits > 0 {
|
|
info.MonthlyCredits = extra.UserStatus.MonthlyPromptCredits
|
|
info.UsedCredits = extra.UserStatus.UserUsedPromptCredits
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
type WindsurfRuntimeInfo struct {
|
|
AccountID int64 `json:"account_id"`
|
|
Tier string `json:"tier"`
|
|
UsagePercent *float64 `json:"usage_percent,omitempty"`
|
|
MonthlyCredits int64 `json:"monthly_credits,omitempty"`
|
|
UsedCredits int64 `json:"used_credits,omitempty"`
|
|
Capabilities map[string]WindsurfModelCapability `json:"capabilities,omitempty"`
|
|
ModelMatrix map[string]WindsurfModelAvail `json:"model_matrix,omitempty"`
|
|
LastProbeAt *string `json:"last_probe_at,omitempty"`
|
|
LastStatusRefreshAt *string `json:"last_status_refresh_at,omitempty"`
|
|
}
|