Files
xiaolajiaoyyds 6f71df4849 Initial open-source release
多平台账号批量注册系统,支持 OpenAI / Grok / Kiro / Gemini 平台的全自动注册。

Features:
- 用户名/密码注册登录(首个用户自动管理员)
- JWT 认证 + bcrypt 密码加密
- 积分系统与多用户管理
- HuggingFace Space 弹性 Worker 节点管理
- Cloudflare Worker 负载均衡
- 实时日志推送(SSE/WebSocket)
- 管理后台(用户管理、系统设置、代理池、卡池)

Tech Stack: Go 1.25 / Gin / GORM / PostgreSQL / Vue 3 / Vite / TailwindCSS
2026-03-22 03:44:42 +08:00

146 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"fmt"
"io"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/xiaolajiaoyyds/regplatformm/internal/service"
)
// WSHandler SSE/WebSocket 日志处理器
type WSHandler struct {
engine *service.TaskEngine
authSvc *service.AuthService
}
// NewWSHandler 创建日志处理器
func NewWSHandler(engine *service.TaskEngine, authSvc *service.AuthService) *WSHandler {
return &WSHandler{engine: engine, authSvc: authSvc}
}
// SSELogs SSE 日志流(GET /ws/logs/:taskId/stream?token=)
func (h *WSHandler) SSELogs(c *gin.Context) {
taskID, _ := strconv.ParseUint(c.Param("taskId"), 10, 32)
token := c.Query("token")
userID, err := h.authSvc.VerifyJWT(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"detail": "未登录"})
return
}
// 等待任务出现(最多 10 秒,感知客户端断开)
var rt *service.RunningTask
timeout := time.After(10 * time.Second)
for {
select {
case <-c.Request.Context().Done():
return
case <-timeout:
c.JSON(http.StatusNotFound, gin.H{"detail": "任务不存在或已结束"})
return
default:
rt = h.engine.FindRunningTaskByID(uint(taskID))
if rt != nil && rt.UserID == userID {
goto found
}
time.Sleep(500 * time.Millisecond)
}
}
found:
if rt == nil || rt.UserID != userID {
c.JSON(http.StatusNotFound, gin.H{"detail": "任务不存在或已结束"})
return
}
// 订阅日志
sub := rt.Subscribe()
defer rt.Unsubscribe(sub)
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no")
// 先发送历史日志(页面刷新后回放)
for _, msg := range rt.GetLogBuffer() {
c.SSEvent("", gin.H{"type": "log", "message": msg})
}
// 发送当前状态快照
c.SSEvent("", gin.H{
"type": "status",
"success": rt.SuccessCount.Load(),
"fail": rt.FailCount.Load(),
"target": rt.Target,
})
c.Writer.Flush()
c.Stream(func(w io.Writer) bool {
select {
case msg, ok := <-sub:
if !ok {
return false
}
c.SSEvent("", gin.H{"type": "log", "message": msg})
return true
case <-rt.StatusCh:
// 成功/失败计数变化,立即推送状态
c.SSEvent("", gin.H{
"type": "status",
"success": rt.SuccessCount.Load(),
"fail": rt.FailCount.Load(),
"target": rt.Target,
})
// StatusCh 与 Done 可能同时就绪,select 随机选中 StatusCh 时需补发 complete
if rt.IsDone() {
elapsed := time.Since(rt.StartedAt).Round(time.Second).String()
c.SSEvent("", gin.H{
"type": "complete",
"success": rt.SuccessCount.Load(),
"fail": rt.FailCount.Load(),
"target": rt.Target,
"elapsed": elapsed,
})
return false
}
return true
case <-time.After(2 * time.Second):
// 心跳 + 状态
c.SSEvent("", gin.H{
"type": "status",
"success": rt.SuccessCount.Load(),
"fail": rt.FailCount.Load(),
"target": rt.Target,
})
return !rt.IsDone()
case <-rt.Done:
elapsed := time.Since(rt.StartedAt).Round(time.Second).String()
c.SSEvent("", gin.H{
"type": "complete",
"success": rt.SuccessCount.Load(),
"fail": rt.FailCount.Load(),
"target": rt.Target,
"elapsed": elapsed,
})
return false
case <-c.Request.Context().Done():
return false
}
})
}
// WebSocketLogs WebSocket 日志(GET /ws/logs/:taskId?token=)
func (h *WSHandler) WebSocketLogs(c *gin.Context) {
// WebSocket 作为 SSE 的降级备选,暂用 SSE 替代
c.JSON(http.StatusOK, gin.H{
"message": "请使用 SSE 端点: /ws/logs/{taskId}/stream?token=",
"sse_url": fmt.Sprintf("/ws/logs/%s/stream", c.Param("taskId")),
})
}