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

584 lines
19 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 service
import (
"context"
"encoding/json"
"errors"
"strings"
"sync"
"time"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/repository"
"gorm.io/gorm"
)
const (
// 心跳会携带本机工作流字段和拓扑;允许大型 ComfyUI 工作流,但保留硬上限。
comfyBridgeCapabilitiesLimit = 4 << 20
// 请求主要是工作流和参考素材描述,保持较小上限控制排队内存;结果可能含 base64 视频,单独放宽但仍有硬上限。
comfyBridgeRequestPayloadLimit = 16 << 20
comfyBridgeResultPayloadLimit = 64 << 20
comfyBridgeMaxPending = 32
comfyBridgeRequestTTL = 2 * time.Hour
comfyBridgePollMaxWait = 60 * time.Second
)
const (
// ComfyBridgeRequestKindGenerate 是现有生成任务的请求类型。空 Kind 会在入队时归一化为该值,
// 这样可以继续兼容已有调用方。
ComfyBridgeRequestKindGenerate = "generate"
// ComfyBridgeRequestKindWorkflowList 和 ComfyBridgeRequestKindWorkflowGet 只用于管理页按需发现。
ComfyBridgeRequestKindWorkflowList = "workflow.list"
ComfyBridgeRequestKindWorkflowGet = "workflow.get"
)
// CreateComfyBridgeRequest 是用户注册本地 Bridge 时提交的公开信息。
// Bridge Token 由服务端生成,不接受客户端自带 Token,避免复用或弱 Token。
type CreateComfyBridgeRequest struct {
Name string `json:"name"`
Capabilities map[string]any `json:"capabilities,omitempty"`
}
type ComfyBridgeSummary struct {
ID string `json:"id"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
Online bool `json:"online"`
LastSeenAt *time.Time `json:"lastSeenAt,omitempty"`
LastTaskAt *time.Time `json:"lastTaskAt,omitempty"`
Capabilities map[string]any `json:"capabilities,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type ComfyBridgeRegistration struct {
Bridge ComfyBridgeSummary `json:"bridge"`
// Token 只在注册响应中出现一次,后续接口不会重新返回明文。
Token string `json:"token"`
}
// ComfyBridgeRequest 是云端后端交给本地 Bridge 的一次请求。
// Payload 的协议由 Bridge 按 Kind 解释;生成请求和管理页发现请求共用同一条长轮询通道。
type ComfyBridgeRequest struct {
ID string `json:"id"`
Kind string `json:"kind"`
TaskID string `json:"taskId,omitempty"`
BridgeID string `json:"bridgeId"`
Payload map[string]any `json:"payload"`
CreatedAt time.Time `json:"createdAt"`
}
type ComfyBridgeCompletion struct {
RequestID string `json:"requestId,omitempty"`
Status string `json:"status"`
Result map[string]any `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
type comfyBridgeEnvelope struct {
request ComfyBridgeRequest
done chan ComfyBridgeCompletion
}
// comfyBridgeQueue 只保存长轮询唤醒和当前进程等待者;请求状态与结果以数据库为准。
type comfyBridgeQueue struct {
mu sync.Mutex
pending map[string]*comfyBridgeEnvelope
notify map[string]chan struct{}
}
var comfyBridgeRuntimes sync.Map // map[*Service]*comfyBridgeQueue
func (s *Service) comfyBridgeRuntime() *comfyBridgeQueue {
if value, ok := comfyBridgeRuntimes.Load(s); ok {
return value.(*comfyBridgeQueue)
}
runtime := &comfyBridgeQueue{
pending: make(map[string]*comfyBridgeEnvelope),
notify: make(map[string]chan struct{}),
}
actual, _ := comfyBridgeRuntimes.LoadOrStore(s, runtime)
return actual.(*comfyBridgeQueue)
}
func (s *Service) CreateComfyBridge(userID string, req CreateComfyBridgeRequest) (*ComfyBridgeRegistration, error) {
if strings.TrimSpace(userID) == "" {
return nil, Unauthorized("请先登录")
}
name := strings.TrimSpace(req.Name)
if name == "" {
name = "本地 ComfyUI Bridge"
}
if len([]rune(name)) > 80 {
return nil, BadAuthRequest("Bridge 名称不能超过 80 个字符")
}
capabilitiesJSON := ""
if req.Capabilities != nil {
encoded, err := json.Marshal(req.Capabilities)
if err != nil {
return nil, BadAuthRequest("Bridge 能力信息格式无效")
}
if len(encoded) > comfyBridgeCapabilitiesLimit {
return nil, BadAuthRequest("Bridge 能力信息不能超过 4MB")
}
capabilitiesJSON = string(encoded)
}
token := randomToken()
now := time.Now()
bridge := &model.ComfyBridge{
ID: newID(),
UserID: userID,
Name: name,
TokenHash: hashToken(token),
Enabled: true,
CapabilitiesJSON: capabilitiesJSON,
CreatedAt: now,
UpdatedAt: now,
}
if err := s.repo.Create(bridge); err != nil {
return nil, err
}
return &ComfyBridgeRegistration{Bridge: comfyBridgeSummary(*bridge), Token: token}, nil
}
func (s *Service) ComfyBridges(userID string) ([]ComfyBridgeSummary, error) {
bridges, err := s.repo.ComfyBridgesForUser(userID)
if err != nil {
return nil, err
}
items := make([]ComfyBridgeSummary, 0, len(bridges))
for _, bridge := range bridges {
items = append(items, comfyBridgeSummary(bridge))
}
return items, nil
}
func (s *Service) RevokeComfyBridge(userID string, bridgeID string) error {
bridgeID = strings.TrimSpace(bridgeID)
if bridgeID == "" {
return BadAuthRequest("Bridge ID 不能为空")
}
if err := s.repo.DisableComfyBridge(userID, bridgeID, time.Now()); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return BadAuthRequest("Bridge 不存在或已撤销")
}
return err
}
_ = s.repo.FailComfyBridgeRequests(bridgeID, "Bridge 已撤销", time.Now())
s.comfyBridgeRuntime().failBridge(bridgeID, errors.New("Bridge 已撤销"))
return nil
}
// AuthenticateComfyBridge 校验 Bridge 专用 Token,并刷新心跳。
func (s *Service) AuthenticateComfyBridge(token string) (*model.ComfyBridge, error) {
token = strings.TrimSpace(token)
if len(token) < 32 || len(token) > 256 {
return nil, Unauthorized("Bridge 令牌无效或已撤销")
}
bridge, err := s.repo.ComfyBridgeByTokenHash(hashToken(token))
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, Unauthorized("Bridge 令牌无效或已撤销")
}
if err != nil {
return nil, err
}
if err := s.repo.TouchComfyBridge(bridge.ID, time.Now()); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, Unauthorized("Bridge 令牌无效或已撤销")
}
return nil, err
}
return bridge, nil
}
func (s *Service) TouchComfyBridgeHeartbeat(bridgeID string, capabilities map[string]any) error {
capabilitiesJSON := ""
if capabilities != nil {
encoded, err := json.Marshal(capabilities)
if err != nil || len(encoded) > comfyBridgeCapabilitiesLimit {
return BadAuthRequest("Bridge 能力信息格式无效或超过 4MB")
}
capabilitiesJSON = string(encoded)
}
if err := s.repo.UpdateComfyBridgeHeartbeat(strings.TrimSpace(bridgeID), capabilitiesJSON, time.Now()); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return Unauthorized("Bridge 令牌无效或已撤销")
}
return err
}
return nil
}
// EnqueueComfyBridgeRequest 将工作流请求持久化,并返回请求 ID。
func (s *Service) EnqueueComfyBridgeRequest(ctx context.Context, userID string, bridgeID string, taskID string, payload map[string]any) (*ComfyBridgeRequest, error) {
return s.enqueueComfyBridgeRequest(ctx, userID, bridgeID, taskID, "", payload)
}
func (s *Service) enqueueComfyBridgeRequest(ctx context.Context, userID string, bridgeID string, taskID string, requestID string, payload map[string]any) (*ComfyBridgeRequest, error) {
if err := contextError(ctx); err != nil {
return nil, err
}
bridgeID = strings.TrimSpace(bridgeID)
taskID = strings.TrimSpace(taskID)
if bridgeID == "" || taskID == "" {
return nil, BadAuthRequest("Bridge ID 和任务 ID 不能为空")
}
if payload == nil {
return nil, BadAuthRequest("Bridge 请求内容不能为空")
}
bridge, err := s.repo.ComfyBridgeForUser(userID, bridgeID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, BadAuthRequest("Bridge 不存在或已撤销")
}
return nil, err
}
if !bridge.Enabled {
return nil, BadAuthRequest("Bridge 不存在或已撤销")
}
encoded, err := json.Marshal(payload)
if err != nil {
return nil, BadAuthRequest("Bridge 请求内容格式无效")
}
if len(encoded) > comfyBridgeRequestPayloadLimit {
return nil, BadAuthRequest("Bridge 请求内容超过 16MB")
}
// Marshal/Unmarshal 生成独立副本,避免调用方在入队后继续修改 map 导致任务内容漂移。
var copied map[string]any
if err := json.Unmarshal(encoded, &copied); err != nil {
return nil, BadAuthRequest("Bridge 请求内容必须是 JSON 对象")
}
requestID = strings.TrimSpace(requestID)
if requestID == "" {
if count, err := s.repo.PendingComfyBridgeRequestCount(bridgeID, time.Now()); err != nil {
return nil, err
} else if count >= comfyBridgeMaxPending {
return nil, BadAuthRequest("该 Bridge 待处理请求过多,请稍后重试")
}
requestID = newID()
}
now := time.Now()
record, created, err := s.repo.CreateOrGetComfyBridgeRequest(&model.ComfyBridgeRequest{
ID: requestID, TaskID: taskID, UserID: userID, BridgeID: bridgeID, Kind: ComfyBridgeRequestKindGenerate,
Status: "queued", PayloadJSON: string(encoded), ExpiresAt: now.Add(comfyBridgeRequestTTL), CreatedAt: now, UpdatedAt: now,
})
if err != nil {
return nil, err
}
if record.UserID != userID || record.BridgeID != bridgeID || record.TaskID != taskID {
return nil, Forbidden("Bridge 请求 ID 已被其他任务使用")
}
request, err := comfyBridgeRequestFromModel(*record)
if err != nil {
return nil, err
}
runtime := s.comfyBridgeRuntime()
runtime.ensure(request)
if created {
runtime.signal(bridgeID)
}
if err := s.repo.MarkComfyBridgeTask(bridgeID, now); err != nil {
if created {
_ = s.repo.CancelComfyBridgeRequest(request.ID, time.Now())
runtime.remove(request.ID)
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, BadAuthRequest("Bridge 不存在或已撤销")
}
return nil, err
}
return request, nil
}
// PollComfyBridgeRequest 由本地 Bridge 调用。wait 为 0 时只检查当前队列。
func (s *Service) PollComfyBridgeRequest(ctx context.Context, bridge *model.ComfyBridge, wait time.Duration) (*ComfyBridgeRequest, error) {
if ctx == nil {
ctx = context.Background()
}
if bridge == nil || strings.TrimSpace(bridge.ID) == "" || !bridge.Enabled {
return nil, Unauthorized("Bridge 令牌无效或已撤销")
}
if wait < 0 {
wait = 0
}
if wait > comfyBridgePollMaxWait {
wait = comfyBridgePollMaxWait
}
runtime := s.comfyBridgeRuntime()
deadline := time.Now().Add(wait)
for {
record, err := s.repo.ClaimNextComfyBridgeRequest(bridge.ID, time.Now())
if err != nil {
return nil, err
}
if record != nil {
request, err := comfyBridgeRequestFromModel(*record)
if err != nil {
_ = s.repo.CancelComfyBridgeRequest(record.ID, time.Now())
return nil, err
}
runtime.ensure(request)
if err := s.repo.MarkComfyBridgeTask(bridge.ID, time.Now()); err != nil {
_ = s.repo.CancelComfyBridgeRequest(request.ID, time.Now())
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, Unauthorized("Bridge 令牌无效或已撤销")
}
return nil, err
}
return request, nil
}
if wait == 0 {
return nil, nil
}
remaining := time.Until(deadline)
if remaining <= 0 {
return nil, nil
}
channel := runtime.channel(bridge.ID)
timer := time.NewTimer(remaining)
select {
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
return nil, ctx.Err()
case <-timer.C:
return nil, nil
case <-channel:
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
}
}
}
// CompleteComfyBridgeRequest 将 Bridge 结果投递给等待该请求的后端 worker。
func (s *Service) CompleteComfyBridgeRequest(bridgeID string, completion ComfyBridgeCompletion) error {
bridgeID = strings.TrimSpace(bridgeID)
completion.RequestID = strings.TrimSpace(completion.RequestID)
if bridgeID == "" || completion.RequestID == "" {
return BadAuthRequest("Bridge ID 和请求 ID 不能为空")
}
completion.Status = strings.ToLower(strings.TrimSpace(completion.Status))
if completion.Status == "" {
completion.Status = "succeeded"
}
switch completion.Status {
case "succeeded", "failed", "cancelled":
default:
return BadAuthRequest("Bridge 结果状态无效")
}
if len([]rune(completion.Error)) > 4000 {
completion.Error = truncateRunes(completion.Error, 4000)
}
if completion.Result != nil {
encoded, err := json.Marshal(completion.Result)
if err != nil || len(encoded) > comfyBridgeResultPayloadLimit {
return BadAuthRequest("Bridge 结果内容无效或超过 64MB")
}
}
resultJSON := ""
if completion.Result != nil {
encoded, _ := json.Marshal(completion.Result)
resultJSON = string(encoded)
}
if _, err := s.repo.CompleteComfyBridgeRequest(bridgeID, completion.RequestID, completion.Status, resultJSON, completion.Error, time.Now()); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return BadAuthRequest("Bridge 请求不存在或已过期")
}
if errors.Is(err, repository.ErrComfyBridgeRequestOwnership) {
return Forbidden("Bridge 不能提交其他设备的请求")
}
if errors.Is(err, repository.ErrComfyBridgeRequestState) {
return BadAuthRequest("Bridge 请求尚未领取")
}
return err
}
runtime := s.comfyBridgeRuntime()
runtime.mu.Lock()
envelope := runtime.pending[completion.RequestID]
if envelope != nil {
select {
case envelope.done <- completion:
default:
}
}
runtime.mu.Unlock()
return nil
}
// WaitComfyBridgeRequest 由生成 worker 调用,直到 Bridge 回传结果或任务被取消。
func (s *Service) WaitComfyBridgeRequest(ctx context.Context, requestID string) (ComfyBridgeCompletion, error) {
if ctx == nil {
ctx = context.Background()
}
requestID = strings.TrimSpace(requestID)
if requestID == "" {
return ComfyBridgeCompletion{}, BadAuthRequest("Bridge 请求 ID 不能为空")
}
runtime := s.comfyBridgeRuntime()
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
record, err := s.repo.ComfyBridgeRequest(requestID)
if errors.Is(err, gorm.ErrRecordNotFound) {
return ComfyBridgeCompletion{}, BadAuthRequest("Bridge 请求不存在或已过期")
}
if err != nil {
return ComfyBridgeCompletion{}, err
}
completion, terminal, err := comfyBridgeCompletionFromModel(*record)
if err != nil {
return ComfyBridgeCompletion{}, err
}
if terminal {
runtime.remove(requestID)
return completion, nil
}
request, err := comfyBridgeRequestFromModel(*record)
if err != nil {
return ComfyBridgeCompletion{}, err
}
envelope := runtime.ensure(request)
select {
case <-envelope.done:
// 数据库已经在结果接口事务中落盘;下一轮统一从持久化记录读取。
case <-ticker.C:
case <-ctx.Done():
_ = s.repo.CancelComfyBridgeRequest(requestID, time.Now())
runtime.remove(requestID)
return ComfyBridgeCompletion{}, ctx.Err()
}
}
}
// CancelComfyBridgeRequest 供任务取消路径清理尚未完成的 Bridge 请求。
func (s *Service) CancelComfyBridgeRequest(requestID string) {
requestID = strings.TrimSpace(requestID)
if requestID == "" {
return
}
_ = s.repo.CancelComfyBridgeRequest(requestID, time.Now())
runtime := s.comfyBridgeRuntime()
runtime.remove(requestID)
}
func comfyBridgeRequestFromModel(record model.ComfyBridgeRequest) (*ComfyBridgeRequest, error) {
payload := map[string]any{}
if err := json.Unmarshal([]byte(record.PayloadJSON), &payload); err != nil {
return nil, errors.New("Bridge 持久化请求内容无效")
}
return &ComfyBridgeRequest{ID: record.ID, Kind: record.Kind, TaskID: record.TaskID, BridgeID: record.BridgeID, Payload: payload, CreatedAt: record.CreatedAt}, nil
}
func comfyBridgeCompletionFromModel(record model.ComfyBridgeRequest) (ComfyBridgeCompletion, bool, error) {
if record.Status != "succeeded" && record.Status != "failed" && record.Status != "cancelled" {
return ComfyBridgeCompletion{}, false, nil
}
completion := ComfyBridgeCompletion{RequestID: record.ID, Status: record.Status, Error: record.Error}
if strings.TrimSpace(record.ResultJSON) != "" {
if err := json.Unmarshal([]byte(record.ResultJSON), &completion.Result); err != nil {
return ComfyBridgeCompletion{}, false, errors.New("Bridge 持久化结果内容无效")
}
}
return completion, true, nil
}
func (q *comfyBridgeQueue) ensure(request *ComfyBridgeRequest) *comfyBridgeEnvelope {
q.mu.Lock()
defer q.mu.Unlock()
if envelope := q.pending[request.ID]; envelope != nil {
return envelope
}
envelope := &comfyBridgeEnvelope{request: *request, done: make(chan ComfyBridgeCompletion, 1)}
q.pending[request.ID] = envelope
return envelope
}
func (q *comfyBridgeQueue) channel(bridgeID string) <-chan struct{} {
q.mu.Lock()
defer q.mu.Unlock()
channel := q.notify[bridgeID]
if channel == nil {
channel = make(chan struct{}, 1)
q.notify[bridgeID] = channel
}
return channel
}
func (q *comfyBridgeQueue) signal(bridgeID string) {
q.mu.Lock()
q.signalLocked(bridgeID)
q.mu.Unlock()
}
func (q *comfyBridgeQueue) remove(requestID string) {
q.mu.Lock()
defer q.mu.Unlock()
delete(q.pending, requestID)
}
func (q *comfyBridgeQueue) failBridge(bridgeID string, err error) {
q.mu.Lock()
defer q.mu.Unlock()
message := "Bridge 请求失败"
if err != nil {
message = err.Error()
}
for id, envelope := range q.pending {
if envelope == nil {
delete(q.pending, id)
continue
}
if envelope.request.BridgeID != bridgeID {
continue
}
select {
case envelope.done <- ComfyBridgeCompletion{RequestID: id, Status: "failed", Error: message}:
default:
}
delete(q.pending, id)
}
q.signalLocked(bridgeID)
}
func (q *comfyBridgeQueue) signalLocked(bridgeID string) {
channel := q.notify[bridgeID]
if channel == nil {
channel = make(chan struct{}, 1)
q.notify[bridgeID] = channel
}
select {
case channel <- struct{}{}:
default:
}
}
func comfyBridgeSummary(bridge model.ComfyBridge) ComfyBridgeSummary {
result := ComfyBridgeSummary{ID: bridge.ID, Name: bridge.Name, Enabled: bridge.Enabled, LastSeenAt: bridge.LastSeenAt, LastTaskAt: bridge.LastTaskAt, CreatedAt: bridge.CreatedAt, UpdatedAt: bridge.UpdatedAt}
result.Online = bridge.Enabled && bridge.LastSeenAt != nil && time.Since(*bridge.LastSeenAt) <= 90*time.Second
if strings.TrimSpace(bridge.CapabilitiesJSON) != "" {
var capabilities map[string]any
if json.Unmarshal([]byte(bridge.CapabilitiesJSON), &capabilities) == nil && capabilities != nil {
result.Capabilities = capabilities
}
}
return result
}
func contextError(ctx context.Context) error {
if ctx == nil {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
default:
return nil
}
}