584 lines
19 KiB
Go
584 lines
19 KiB
Go
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
|
||
}
|
||
}
|