- windsurf_gateway_service: 添加上游延迟/TTFT/错误上下文记录 - endpoint: DeriveUpstreamEndpoint 添加 PlatformWindsurf 分支 - ops_error_logger: guessPlatformFromPath 添加 /windsurf/ 识别
358 lines
9.5 KiB
Go
358 lines
9.5 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 WindsurfLSService struct {
|
|
cfg config.WindsurfConfig
|
|
connector windsurf.LSConnector
|
|
}
|
|
|
|
func NewWindsurfLSService(cfg config.WindsurfConfig, pool *windsurf.LSPool) *WindsurfLSService {
|
|
var connector windsurf.LSConnector
|
|
|
|
switch cfg.LSMode {
|
|
case "docker":
|
|
connector = windsurf.NewCompatDockerConnector(
|
|
cfg.Docker.Host,
|
|
cfg.Docker.Port,
|
|
windsurf.DockerDiscoveryConfig{
|
|
DefaultCSRFToken: cfg.Docker.CSRFToken,
|
|
ProbeInterval: cfg.Docker.ProbeInterval,
|
|
ProbeTimeout: cfg.Docker.ProbeTimeout,
|
|
DiscoverInterval: cfg.Docker.DiscoverInterval,
|
|
},
|
|
)
|
|
case "embedded":
|
|
connector = windsurf.NewEmbeddedConnector(pool)
|
|
case "external":
|
|
port := 0
|
|
if cfg.External.BaseURL != "" {
|
|
port = 443
|
|
}
|
|
connector = windsurf.NewExternalConnector(
|
|
cfg.External.BaseURL,
|
|
port,
|
|
cfg.External.CSRFToken,
|
|
)
|
|
default:
|
|
connector = windsurf.NewDockerConnector(
|
|
cfg.Docker.Host,
|
|
cfg.Docker.Port,
|
|
cfg.Docker.CSRFToken,
|
|
)
|
|
slog.Warn("windsurf_ls_unknown_mode", "mode", cfg.LSMode, "fallback", "docker")
|
|
}
|
|
|
|
return &WindsurfLSService{
|
|
cfg: cfg,
|
|
connector: connector,
|
|
}
|
|
}
|
|
|
|
func (s *WindsurfLSService) Connector() windsurf.LSConnector {
|
|
return s.connector
|
|
}
|
|
|
|
func (s *WindsurfLSService) Acquire(ctx context.Context, proxyURL string) (*windsurf.LSLease, error) {
|
|
return s.connector.Acquire(ctx, proxyURL)
|
|
}
|
|
|
|
func (s *WindsurfLSService) AcquireByBinding(binding WindsurfLSBinding) (*windsurf.LSLease, error) {
|
|
if binding.ContainerID == "" && binding.ContainerName == "" {
|
|
return s.connector.Acquire(context.Background(), "")
|
|
}
|
|
if dc, ok := s.connector.(*windsurf.DockerDiscoveryConnector); ok {
|
|
id := binding.ContainerID
|
|
if id == "" {
|
|
id = binding.ContainerName
|
|
}
|
|
return dc.AcquireByID(id)
|
|
}
|
|
return s.connector.Acquire(context.Background(), "")
|
|
}
|
|
|
|
func (s *WindsurfLSService) Health(ctx context.Context) error {
|
|
return s.connector.Health(ctx)
|
|
}
|
|
|
|
func (s *WindsurfLSService) Status() *windsurf.LSConnectorStatus {
|
|
return s.connector.Status()
|
|
}
|
|
|
|
type WindsurfAuthService struct {
|
|
cfg config.WindsurfConfig
|
|
authClient *windsurf.AuthClient
|
|
accountRepo AccountRepository
|
|
proxyRepo ProxyRepository
|
|
adminSvc AdminService
|
|
}
|
|
|
|
func NewWindsurfAuthService(
|
|
cfg config.WindsurfConfig,
|
|
accountRepo AccountRepository,
|
|
proxyRepo ProxyRepository,
|
|
adminSvc AdminService,
|
|
) *WindsurfAuthService {
|
|
authClient := &windsurf.AuthClient{
|
|
Auth1BaseURL: cfg.Auth1BaseURL,
|
|
SeatServiceBaseURL: cfg.SeatServiceBaseURL,
|
|
CodeiumRegisterURL: cfg.CodeiumRegisterURL,
|
|
FirebaseAPIKey: cfg.FirebaseAPIKey,
|
|
RequestTimeout: cfg.RequestTimeout,
|
|
}
|
|
return &WindsurfAuthService{
|
|
cfg: cfg,
|
|
authClient: authClient,
|
|
accountRepo: accountRepo,
|
|
proxyRepo: proxyRepo,
|
|
adminSvc: adminSvc,
|
|
}
|
|
}
|
|
|
|
type WindsurfLoginInput struct {
|
|
Email string
|
|
Password string
|
|
Name string
|
|
Notes *string
|
|
ProxyID *int64
|
|
GroupIDs []int64
|
|
Concurrency int
|
|
Priority int
|
|
ProbeAfter bool
|
|
LSInstanceID string
|
|
}
|
|
|
|
type WindsurfLoginOutput struct {
|
|
AccountID int64 `json:"account_id"`
|
|
Email string `json:"email"`
|
|
Tier string `json:"tier"`
|
|
AuthMethod string `json:"auth_method"`
|
|
APIKeyPresent bool `json:"api_key_present"`
|
|
RefreshTokenPresent bool `json:"refresh_token_present"`
|
|
}
|
|
|
|
func (s *WindsurfAuthService) Login(ctx context.Context, input *WindsurfLoginInput) (*WindsurfLoginOutput, error) {
|
|
existing, err := s.accountRepo.FindByCredentialField(ctx, domain.PlatformWindsurf, "email", input.Email)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("check existing account: %w", err)
|
|
}
|
|
if len(existing) > 0 {
|
|
return nil, fmt.Errorf("windsurf account with email %s already exists (account_id=%d)", input.Email, existing[0].ID)
|
|
}
|
|
|
|
proxyURL := ""
|
|
if input.ProxyID != nil {
|
|
proxy, err := s.proxyRepo.GetByID(ctx, *input.ProxyID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get proxy: %w", err)
|
|
}
|
|
proxyURL = proxy.URL()
|
|
}
|
|
|
|
result, err := s.authClient.Login(ctx, input.Email, input.Password, proxyURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
creds := WindsurfCredentials{
|
|
Email: input.Email,
|
|
APIKey: result.APIKey,
|
|
RefreshToken: result.RefreshToken,
|
|
IDToken: result.IDToken,
|
|
SessionToken: result.SessionToken,
|
|
Auth1Token: result.Auth1Token,
|
|
AuthMethod: result.AuthMethod,
|
|
APIServerURL: result.APIServerURL,
|
|
RegisteredAt: time.Now().Format(time.RFC3339),
|
|
}
|
|
|
|
expiresAt := time.Now().Add(50 * time.Minute)
|
|
creds.ExpiresAt = expiresAt.Format(time.RFC3339)
|
|
|
|
credMap := StoreWindsurfCredentials(creds)
|
|
|
|
extra := WindsurfExtra{
|
|
Profile: WindsurfProfileSnapshot{
|
|
TierSource: "login",
|
|
},
|
|
Refresh: WindsurfRefreshState{},
|
|
}
|
|
if input.LSInstanceID != "" {
|
|
extra.LSBinding = WindsurfLSBinding{
|
|
ContainerID: input.LSInstanceID,
|
|
}
|
|
}
|
|
extraMap := StoreWindsurfExtra(extra)
|
|
|
|
name := input.Name
|
|
if name == "" {
|
|
if result.Name != "" {
|
|
name = result.Name
|
|
} else {
|
|
name = input.Email
|
|
}
|
|
}
|
|
|
|
concurrency := input.Concurrency
|
|
if concurrency <= 0 {
|
|
concurrency = 1
|
|
}
|
|
|
|
createInput := &CreateAccountInput{
|
|
Name: name,
|
|
Notes: input.Notes,
|
|
Platform: domain.PlatformWindsurf,
|
|
Type: domain.AccountTypeWindsurfSession,
|
|
Credentials: credMap,
|
|
Extra: extraMap,
|
|
ProxyID: input.ProxyID,
|
|
Concurrency: concurrency,
|
|
Priority: input.Priority,
|
|
GroupIDs: input.GroupIDs,
|
|
}
|
|
|
|
account, err := s.adminSvc.CreateAccount(ctx, createInput)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create account: %w", err)
|
|
}
|
|
|
|
return &WindsurfLoginOutput{
|
|
AccountID: account.ID,
|
|
Email: input.Email,
|
|
Tier: "unknown",
|
|
AuthMethod: result.AuthMethod,
|
|
APIKeyPresent: result.APIKey != "",
|
|
RefreshTokenPresent: result.RefreshToken != "",
|
|
}, nil
|
|
}
|
|
|
|
func (s *WindsurfAuthService) BatchLogin(ctx context.Context, items []string, proxyID *int64, groupIDs []int64, concurrency, priority int, probeAfter bool) ([]WindsurfBatchResult, error) {
|
|
results := make([]WindsurfBatchResult, 0, len(items))
|
|
|
|
for _, item := range items {
|
|
email, password, err := parseEmailPassword(item)
|
|
if err != nil {
|
|
results = append(results, WindsurfBatchResult{
|
|
Email: item,
|
|
Success: false,
|
|
Error: err.Error(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
input := &WindsurfLoginInput{
|
|
Email: email,
|
|
Password: password,
|
|
ProxyID: proxyID,
|
|
GroupIDs: groupIDs,
|
|
Concurrency: concurrency,
|
|
Priority: priority,
|
|
ProbeAfter: probeAfter,
|
|
}
|
|
|
|
output, loginErr := s.Login(ctx, input)
|
|
if loginErr != nil {
|
|
results = append(results, WindsurfBatchResult{
|
|
Email: email,
|
|
Success: false,
|
|
Error: loginErr.Error(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
results = append(results, WindsurfBatchResult{
|
|
Email: email,
|
|
Success: true,
|
|
AccountID: output.AccountID,
|
|
Output: output,
|
|
})
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
type WindsurfBatchResult struct {
|
|
Email string `json:"email"`
|
|
Success bool `json:"success"`
|
|
AccountID int64 `json:"account_id,omitempty"`
|
|
Output *WindsurfLoginOutput `json:"output,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
func parseEmailPassword(item string) (string, string, error) {
|
|
sep := "----"
|
|
idx := -1
|
|
for i := 0; i <= len(item)-len(sep); i++ {
|
|
if item[i:i+len(sep)] == sep {
|
|
idx = i
|
|
break
|
|
}
|
|
}
|
|
if idx < 0 {
|
|
return "", "", fmt.Errorf("invalid format: expected email----password")
|
|
}
|
|
email := item[:idx]
|
|
password := item[idx+len(sep):]
|
|
if email == "" || password == "" {
|
|
return "", "", fmt.Errorf("email and password cannot be empty")
|
|
}
|
|
return email, password, nil
|
|
}
|
|
|
|
func (s *WindsurfAuthService) RefreshToken(ctx context.Context, accountID int64) error {
|
|
account, err := s.accountRepo.GetByID(ctx, accountID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if account.Platform != domain.PlatformWindsurf {
|
|
return fmt.Errorf("account %d is not a windsurf account", accountID)
|
|
}
|
|
|
|
creds := LoadWindsurfCredentials(account.Credentials)
|
|
proxyURL := ""
|
|
if account.ProxyID != nil {
|
|
proxy, err := s.proxyRepo.GetByID(ctx, *account.ProxyID)
|
|
if err == nil {
|
|
proxyURL = proxy.URL()
|
|
}
|
|
}
|
|
|
|
if creds.AuthMethod == "firebase" && creds.RefreshToken != "" {
|
|
refreshResult, err := s.authClient.RefreshFirebaseToken(ctx, creds.RefreshToken, proxyURL)
|
|
if err != nil {
|
|
return fmt.Errorf("firebase refresh: %w", err)
|
|
}
|
|
|
|
creds.IDToken = refreshResult.IDToken
|
|
creds.RefreshToken = refreshResult.RefreshToken
|
|
creds.ExpiresAt = time.Now().Add(time.Duration(refreshResult.ExpiresIn) * time.Second).Format(time.RFC3339)
|
|
creds.LastRefreshAt = time.Now().Format(time.RFC3339)
|
|
|
|
regResult, err := s.authClient.ReRegisterWithCodeium(ctx, refreshResult.IDToken, proxyURL)
|
|
if err != nil {
|
|
slog.Warn("windsurf_reregister_failed", "account_id", accountID, "error", err)
|
|
} else {
|
|
creds.APIKey = regResult.APIKey
|
|
creds.LastReregisterAt = time.Now().Format(time.RFC3339)
|
|
}
|
|
} else if creds.AuthMethod == "auth1" {
|
|
// Auth1 tokens don't use Firebase refresh; re-login would be needed
|
|
return fmt.Errorf("auth1 accounts require re-login for token refresh")
|
|
} else {
|
|
return fmt.Errorf("unknown auth method: %s", creds.AuthMethod)
|
|
}
|
|
|
|
credMap := StoreWindsurfCredentials(creds)
|
|
account.Credentials = credMap
|
|
return s.accountRepo.Update(ctx, account)
|
|
}
|