sub2api/backend/internal/handler/admin/windsurf_handler.go
win de048fad25 chore(wip): save Windsurf/Antigravity/ops customizations before upstream merge
WIP commit保存以下定制工作以便后续合并 upstream v0.1.124-125:
- Windsurf: tier access service, NLU extractor, cold threshold, Google login
- Antigravity: client/oauth 调整
- Ops: log stream handler/broadcaster/middleware, OpsLogStreamView
- Frontend: WindsurfLoginModal Google, GoogleIcon, AccountsView, sidebar/router/i18n
2026-05-09 00:41:19 +08:00

385 lines
9.7 KiB
Go

package admin
import (
"net/http"
"strconv"
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/pkg/windsurf"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
type WindsurfHandler struct {
authService *service.WindsurfAuthService
lsService *service.WindsurfLSService
probeService *service.WindsurfProbeService
tierAccessService *service.WindsurfTierAccessService
}
func NewWindsurfHandler(
authService *service.WindsurfAuthService,
lsService *service.WindsurfLSService,
probeService *service.WindsurfProbeService,
tierAccessService *service.WindsurfTierAccessService,
) *WindsurfHandler {
return &WindsurfHandler{
authService: authService,
lsService: lsService,
probeService: probeService,
tierAccessService: tierAccessService,
}
}
func (h *WindsurfHandler) Login(c *gin.Context) {
var req dto.WindsurfLoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
concurrency := 1
if req.Concurrency != nil && *req.Concurrency > 0 {
concurrency = *req.Concurrency
}
priority := 0
if req.Priority != nil {
priority = *req.Priority
}
probeAfter := false
if req.ProbeAfter != nil {
probeAfter = *req.ProbeAfter
}
input := &service.WindsurfLoginInput{
Email: req.Email,
Password: req.Password,
Name: req.Name,
Notes: req.Notes,
ProxyID: req.ProxyID,
GroupIDs: req.GroupIDs,
Concurrency: concurrency,
Priority: priority,
ProbeAfter: probeAfter,
LSInstanceID: req.LSInstanceID,
}
output, err := h.authService.Login(c.Request.Context(), input)
if err != nil {
response.Error(c, http.StatusInternalServerError, err.Error())
return
}
response.Success(c, dto.WindsurfLoginResponse{
AccountID: output.AccountID,
Platform: "windsurf",
Type: "windsurf-session",
Email: output.Email,
Tier: output.Tier,
AuthMethod: output.AuthMethod,
APIKeyPresent: output.APIKeyPresent,
RefreshTokenPresent: output.RefreshTokenPresent,
})
}
func (h *WindsurfHandler) TokenLogin(c *gin.Context) {
var req dto.WindsurfTokenLoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
concurrency := 1
if req.Concurrency != nil && *req.Concurrency > 0 {
concurrency = *req.Concurrency
}
priority := 0
if req.Priority != nil {
priority = *req.Priority
}
probeAfter := false
if req.ProbeAfter != nil {
probeAfter = *req.ProbeAfter
}
input := &service.WindsurfTokenLoginInput{
Token: req.Token,
Email: req.Email,
Name: req.Name,
Notes: req.Notes,
ProxyID: req.ProxyID,
GroupIDs: req.GroupIDs,
Concurrency: concurrency,
Priority: priority,
ProbeAfter: probeAfter,
LSInstanceID: req.LSInstanceID,
}
output, err := h.authService.TokenLogin(c.Request.Context(), input)
if err != nil {
// ErrorFrom maps typed ApplicationError (BadRequest/Conflict/etc.)
// to its real HTTP code; falls through to 500 for opaque errors.
if !response.ErrorFrom(c, err) {
response.Error(c, http.StatusInternalServerError, err.Error())
}
return
}
response.Success(c, dto.WindsurfLoginResponse{
AccountID: output.AccountID,
Platform: "windsurf",
Type: "windsurf-session",
Email: output.Email,
Tier: output.Tier,
AuthMethod: output.AuthMethod,
APIKeyPresent: output.APIKeyPresent,
RefreshTokenPresent: output.RefreshTokenPresent,
})
}
func (h *WindsurfHandler) BatchLogin(c *gin.Context) {
var req dto.WindsurfBatchLoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
concurrency := 1
if req.Concurrency != nil && *req.Concurrency > 0 {
concurrency = *req.Concurrency
}
priority := 0
if req.Priority != nil {
priority = *req.Priority
}
probeAfter := false
if req.ProbeAfter != nil {
probeAfter = *req.ProbeAfter
}
results, err := h.authService.BatchLogin(
c.Request.Context(),
req.Items,
req.ProxyID,
req.GroupIDs,
concurrency,
priority,
probeAfter,
)
if err != nil {
response.Error(c, http.StatusInternalServerError, err.Error())
return
}
successCount := 0
failCount := 0
batchResults := make([]dto.WindsurfBatchLoginResult, 0, len(results))
for _, r := range results {
br := dto.WindsurfBatchLoginResult{
Email: r.Email,
Success: r.Success,
Error: r.Error,
}
if r.Success && r.Output != nil {
successCount++
br.Account = &dto.WindsurfLoginResponse{
AccountID: r.Output.AccountID,
Platform: "windsurf",
Type: "windsurf-session",
Email: r.Output.Email,
Tier: r.Output.Tier,
AuthMethod: r.Output.AuthMethod,
APIKeyPresent: r.Output.APIKeyPresent,
RefreshTokenPresent: r.Output.RefreshTokenPresent,
}
} else {
failCount++
}
batchResults = append(batchResults, br)
}
response.Success(c, dto.WindsurfBatchLoginResponse{
Results: batchResults,
Total: len(results),
SuccessCount: successCount,
FailCount: failCount,
})
}
func (h *WindsurfHandler) RefreshToken(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
response.BadRequest(c, "invalid account id")
return
}
if err := h.authService.RefreshToken(c.Request.Context(), id); err != nil {
response.Error(c, http.StatusInternalServerError, err.Error())
return
}
response.Success(c, dto.WindsurfRefreshTokenResponse{
Refreshed: true,
})
}
func (h *WindsurfHandler) BatchRefreshTokens(c *gin.Context) {
var req dto.WindsurfBatchIDsRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
successCount := 0
failCount := 0
for _, id := range req.AccountIDs {
if err := h.authService.RefreshToken(c.Request.Context(), id); err != nil {
failCount++
} else {
successCount++
}
}
response.Success(c, gin.H{
"total": len(req.AccountIDs),
"success_count": successCount,
"fail_count": failCount,
})
}
func (h *WindsurfHandler) GetLSStatus(c *gin.Context) {
if h.lsService == nil {
response.Success(c, dto.WindsurfLSStatusResponse{
Mode: "disabled",
Healthy: false,
})
return
}
status := h.lsService.Status()
resp := dto.WindsurfLSStatusResponse{
Mode: status.Mode,
Healthy: status.Healthy,
Instances: status.Instances,
Endpoint: status.Endpoint,
}
if dc, ok := h.lsService.Connector().(*windsurf.DockerDiscoveryConnector); ok {
for _, inst := range dc.InstanceStatuses() {
resp.Details = append(resp.Details, dto.WindsurfLSInstanceDetail{
ContainerID: inst.ContainerID,
ContainerName: inst.ContainerName,
Host: inst.Host,
Port: inst.Port,
Healthy: inst.Healthy,
DiscoveredAt: inst.DiscoveredAt.Format("2006-01-02T15:04:05Z07:00"),
LastProbeAt: inst.LastProbeAt.Format("2006-01-02T15:04:05Z07:00"),
LastProbeErr: inst.LastProbeErr,
})
}
}
response.Success(c, resp)
}
func (h *WindsurfHandler) ListModels(c *gin.Context) {
models := windsurf.ListModelsOpenAI()
response.Success(c, models)
}
func (h *WindsurfHandler) Probe(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
response.BadRequest(c, "invalid account id")
return
}
result, err := h.probeService.ProbeAccount(c.Request.Context(), id)
if err != nil {
response.Error(c, http.StatusInternalServerError, err.Error())
return
}
response.Success(c, result)
}
func (h *WindsurfHandler) BatchProbe(c *gin.Context) {
var req dto.WindsurfBatchIDsRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
type probeResult struct {
AccountID int64 `json:"account_id"`
Success bool `json:"success"`
Tier string `json:"tier,omitempty"`
Error string `json:"error,omitempty"`
}
results := make([]probeResult, 0, len(req.AccountIDs))
successCount := 0
failCount := 0
for _, id := range req.AccountIDs {
r, err := h.probeService.ProbeAccount(c.Request.Context(), id)
if err != nil {
failCount++
results = append(results, probeResult{AccountID: id, Error: err.Error()})
continue
}
if r.Error != "" {
failCount++
results = append(results, probeResult{AccountID: id, Error: r.Error})
continue
}
successCount++
results = append(results, probeResult{AccountID: id, Success: true, Tier: r.Tier})
}
response.Success(c, gin.H{
"results": results,
"total": len(req.AccountIDs),
"success_count": successCount,
"fail_count": failCount,
})
}
func (h *WindsurfHandler) GetRuntime(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
response.BadRequest(c, "invalid account id")
return
}
result, err := h.probeService.GetRuntime(c.Request.Context(), id)
if err != nil {
response.Error(c, http.StatusInternalServerError, err.Error())
return
}
response.Success(c, result)
}
// GetTierAccess returns per-model account-pool availability for the
// admin dashboard. Backed by a 60s in-memory snapshot cache.
func (h *WindsurfHandler) GetTierAccess(c *gin.Context) {
if h.tierAccessService == nil {
response.Error(c, http.StatusServiceUnavailable, "tier access service not configured")
return
}
snap, err := h.tierAccessService.Snapshot(c.Request.Context())
if err != nil {
response.Error(c, http.StatusInternalServerError, err.Error())
return
}
response.Success(c, snap)
}