Files
ttstd-infinite-canvas/backend/internal/handler/comfy_bridge.go

196 lines
5.6 KiB
Go
Raw 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 (
"context"
"errors"
"io"
"net/http"
"strconv"
"strings"
"time"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/service"
"github.com/gin-gonic/gin"
)
// Bridge 结果通常包含 base64 媒体;保持和 service 层相同的硬上限,避免请求体无限增长。
const maxComfyBridgeRequestBytes int64 = 64 << 20
// Bridge 心跳会携带本机工作流字段和拓扑,允许与 service 层一致的 4MB 上限。
const maxComfyBridgeCapabilitiesBytes int64 = 4 << 20
type comfyBridgeResultRequest struct {
RequestID string `json:"requestId"`
ID string `json:"id,omitempty"`
Status string `json:"status"`
Result map[string]any `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
// RegisterComfyBridgeRoutes 同时注册用户管理接口和 Bridge 专用轮询接口。
// 两套接口使用不同认证方式:用户接口依赖登录 Cookie,Bridge 接口只接受专用 Header Token。
func RegisterComfyBridgeRoutes(r *gin.RouterGroup, svc *service.Service) {
r.POST("/comfy-bridges", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
if err := svc.RequireWorkflowPluginForUser(user.ID, "comfyui-bridge-image"); err != nil {
failService(c, err)
return
}
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 64<<10)
var req service.CreateComfyBridgeRequest
if err := c.ShouldBindJSON(&req); err != nil {
fail(c, http.StatusBadRequest, err)
return
}
result, err := svc.CreateComfyBridge(user.ID, req)
if err != nil {
failService(c, err)
return
}
ok(c, result)
})
r.GET("/comfy-bridges", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
if err := svc.RequireWorkflowPluginForUser(user.ID, "comfyui-bridge-image"); err != nil {
failService(c, err)
return
}
bridges, err := svc.ComfyBridges(user.ID)
if err != nil {
fail(c, http.StatusInternalServerError, err)
return
}
ok(c, bridges)
})
revoke := func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
if err := svc.RevokeComfyBridge(user.ID, c.Param("id")); err != nil {
failService(c, err)
return
}
ok(c, gin.H{"revoked": true})
}
r.DELETE("/comfy-bridges/:id", revoke)
r.POST("/comfy-bridges/:id/revoke", revoke)
r.GET("/comfy-bridge/poll", func(c *gin.Context) {
bridge, err := authenticateComfyBridgeRequest(c, svc)
if err != nil {
failService(c, err)
return
}
wait, err := comfyBridgeWait(c.Query("wait"))
if err != nil {
fail(c, http.StatusBadRequest, err)
return
}
request, err := svc.PollComfyBridgeRequest(c.Request.Context(), bridge, wait)
if err != nil {
if errors.Is(err, context.Canceled) {
return
}
failService(c, err)
return
}
ok(c, gin.H{"request": request})
})
r.POST("/comfy-bridge/result", func(c *gin.Context) {
bridge, err := authenticateComfyBridgeRequest(c, svc)
if err != nil {
failService(c, err)
return
}
if c.Request.ContentLength > maxComfyBridgeRequestBytes {
fail(c, http.StatusRequestEntityTooLarge, errors.New("Bridge 结果请求体超过 64MB"))
return
}
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxComfyBridgeRequestBytes)
var req comfyBridgeResultRequest
if err := c.ShouldBindJSON(&req); err != nil {
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
fail(c, http.StatusRequestEntityTooLarge, errors.New("Bridge 结果请求体超过 64MB"))
return
}
fail(c, http.StatusBadRequest, err)
return
}
requestID := strings.TrimSpace(req.RequestID)
if requestID == "" {
requestID = strings.TrimSpace(req.ID)
}
err = svc.CompleteComfyBridgeRequest(bridge.ID, service.ComfyBridgeCompletion{
RequestID: requestID,
Status: req.Status,
Result: req.Result,
Error: req.Error,
})
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"accepted": true})
})
r.POST("/comfy-bridge/heartbeat", func(c *gin.Context) {
bridge, err := authenticateComfyBridgeRequest(c, svc)
if err != nil {
failService(c, err)
return
}
var req struct {
Capabilities map[string]any `json:"capabilities,omitempty"`
}
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxComfyBridgeCapabilitiesBytes)
if c.Request.ContentLength != 0 {
if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
fail(c, http.StatusBadRequest, err)
return
}
}
if err := svc.TouchComfyBridgeHeartbeat(bridge.ID, req.Capabilities); err != nil {
failService(c, err)
return
}
ok(c, gin.H{"ok": true})
})
}
func authenticateComfyBridgeRequest(c *gin.Context, svc *service.Service) (*model.ComfyBridge, error) {
token := strings.TrimSpace(c.GetHeader("X-Canvas-Comfy-Bridge-Token"))
if token == "" {
token = strings.TrimSpace(c.GetHeader("X-Canvas-Bridge-Token"))
}
if token == "" {
authorization := strings.TrimSpace(c.GetHeader("Authorization"))
if len(authorization) >= 7 && strings.EqualFold(authorization[:7], "Bearer ") {
token = strings.TrimSpace(authorization[7:])
}
}
return svc.AuthenticateComfyBridge(token)
}
func comfyBridgeWait(raw string) (time.Duration, error) {
if strings.TrimSpace(raw) == "" {
return 20 * time.Second, nil
}
seconds, err := strconv.Atoi(strings.TrimSpace(raw))
if err != nil || seconds < 0 || seconds > 60 {
return 0, errors.New("wait 必须是 0 到 60 之间的整数秒")
}
return time.Duration(seconds) * time.Second, nil
}