Skip to Content
Ⅰ · 系统骨架03 · 服务、队列与传输

03 · 服务、队列与传输

PromptServer 看起来像普通 aiohttp 应用,实际承担了四个角色:静态前端宿主、控制 API、实时消息总线、执行器与外部世界之间的适配层。理解它的关键是把 HTTP 请求、队列状态、工作线程、WebSocket 推送 分成四段。

外部协议总览

中间件先建立安全与兼容边界

PromptServer 构造时按配置组合中间件:

  • cache-control:避免动态 API 被错误缓存;
  • deprecated route 提示;
  • 可选响应体压缩;
  • 显式 CORS,或默认的 loopback origin 防护;
  • 禁用 API Nodes 时附加 CSP;
  • Manager 相关中间件。
server.pyL215-245PromptServer 初始化与中间件选择。

默认为什么检查 Host 与 Origin

服务常监听 127.0.0.1,但“只监听本机”不等于浏览器请求天然安全。恶意网页可以尝试让用户浏览器请求本地端口。默认中间件会比较 loopback Host 与 Origin 的主机名,阻止跨站来源利用本机 ComfyUI。

server.pyL160-197本机访问时的 Host / Origin 对照。

显式配置 CORS 是另一种策略,意味着操作者主动声明允许的来源。两者不能当作同一个开关的正反面:宽泛 CORS 会扩大控制 API 的可调用面。

路由不是一张平铺表

可以按资源性质分为六组:

组代表路由状态来源
能力发现/system_stats、/features、/object_info运行时、节点映射
模型与文件/models、/upload/image、/viewfolder_paths、磁盘
执行控制/prompt、/queue、/interrupt、/freePromptQueue、model management
结果查询/history、/jobs内存 history / job facade
用户与内部服务/userdata、/settings、/internal/...DB、文件服务、日志
可选扩展/api/...、Assets、模板、subgraphsManagers、feature flags

add_routes() 还把部分旧路由复制到 /api 前缀,挂载内部子应用、扩展 Web 目录、模板与嵌入式文档,最后才托管前端静态根目录。

server.pyL1220-1281路由汇总、兼容前缀与静态前端托管。

这解释了一个常见误会:本仓库并不包含完整前端开发源码,运行时前端由 FrontendManager 管理的独立包提供;server.py 负责托管和协商版本。

/prompt:控制面最重要的入口

一个请求大致经历:

JSON body ├─ prompt: API Prompt ├─ client_id: 可进入 extra_data ├─ number / front: 队列优先级 ├─ prompt_id: 可由客户端指定或服务端生成 ├─ partial_execution_targets: 可选局部目标 └─ extra_data: 附加上下文(含可能的敏感字段) ↓ node replacements ↓ await validate_prompt(prompt, targets) ↓ PromptQueue.put((number, prompt_id, prompt, extra_data, outputs, sensitive))

队列编号

服务维护递增编号。普通任务使用正向编号;front 可使编号落到当前前方。PromptQueue 用 heap 保存,所以元组第一项决定取出优先级。

这不是持久的业务优先级系统:服务重启后编号与队列都消失,也没有租约、重试次数或跨进程一致性。

验证结果

成功响应返回 prompt_id、number 和节点错误信息;失败时返回结构化 error 与 node_errors。验证先于入队,避免工作线程才发现基本 schema 错误。

敏感字段为什么拆开

代码会把指定的敏感 extra_data 从普通队列项中分离,执行前再恢复。目标是避免它们随常规历史记录或状态序列化扩散。节点和扩展仍需谨慎处理这些值;“分离保存”不等于安全沙箱。

PromptQueue 的状态机

execution.pyL1251-1411RLock、Condition、heap、running、history 与 flags。

三份状态不要混为一谈

  • queue:尚未取出的 heap;
  • currently_running:已经交给 worker 的任务;
  • history:task_done() 后的执行记录,默认最多 10,000 条。

取消等待任务是从 heap 删除;中断运行任务则通过执行中断机制协作。已经在 GPU kernel 内的操作未必能在任意指令点立即停下。

Condition 的意义

worker 没任务时在条件变量上等待,而不是轮询。put() 在锁内压入 heap 后通知。队列所有复合状态变化使用同一 RLock,使“从 waiting 移到 running”对查询者表现为原子操作。

WebSocket 会话

客户端连接 /ws 时可携带 clientId:

  1. 若 ID 已存在,旧连接被替换,可用于重连;
  2. 若没有,服务端生成 session ID;
  3. 服务立即发送当前队列状态;
  4. 双方协商 feature flags / socket metadata;
  5. 连接保存在 sockets[sid],断开时清理当前实例。
server.pyL269-327WebSocket 建连、重连、初始状态和功能协商。

Prompt 中的 client_id 最终用于把执行事件定向给提交者。没有目标 sid 的部分状态也可广播,具体取决于调用点。

消息的两种形态

JSON 事件

典型事件包括:

事件语义
status队列剩余数量等整体状态
execution_start某 Prompt 开始
executing当前节点发生变化;node: null 常表示结束
progress当前采样 step / total、节点与 prompt
executed节点产生 UI 输出
execution_cached哪些节点直接命中缓存
execution_error节点、异常类型、堆栈与输入摘要

二进制事件

预览图不是 base64 JSON。服务把事件类型编码为大端整数前缀,后接图像格式/数据等 payload,再用 WebSocket binary frame 发送。这减少编码膨胀,也让客户端先读事件号再解释负载。

server.pyL1290-1402JSON / 二进制编码、send_sync 与 publish loop。

跨线程桥:最容易被忽略的关键

工作线程不能直接 await socket.send_*(),它既不在 aiohttp 的 event loop,也不应持有连接生命周期。服务采用两段桥:

send_sync() 的名字表示“可从同步上下文调用”,不是“等待网络发送完成”。它把所有 Socket 写操作重新归属给正确的事件循环。

从采样进度到浏览器

sampler callback(step, x0, x, total) → ProgressBar.update_absolute(..., preview) → global progress hook → PromptServer.send_sync('progress', ...) → asyncio message queue → publish_loop → WebSocket client_id

预览可能经过尺寸限制和格式编码。进度 hook 还检查客户端是否支持预览元数据,体现了后端与不同前端版本的功能协商。

错误经过哪几层

发生位置典型错误返回方式
JSON / 路由body 缺字段、非法参数HTTP 4xx JSON
验证节点不存在、类型错误、环/prompt 的 error / node_errors
节点执行Python 异常、OOM、ExecutionBlockerWebSocket execution_error + history status
传输Socket 已断开清理连接;执行可继续
文件服务路径越界、危险类型拒绝或强制下载

Socket 断开不自动等于取消 Prompt。执行任务属于队列,不属于 WebSocket 对象;这是 API 客户端可重连查询结果的基础。

设计取舍与边界

单 worker 的好处

模型复用、显存状态和节点副作用更易推理,不会让两个采样任务无意争抢同一套 patcher。代价是实例内吞吐依赖批处理或外部横向扩容,而非简单并发多个 Prompt。

内存队列的好处

低延迟、实现简单、可直接携带 Python 对象。代价是不耐进程故障、不能跨实例协调。生产任务系统应把 ComfyUI 当执行后端,在外层做 durable orchestration。

HTTP + WebSocket 的好处

提交、查询与资源传输留在可缓存/可重试的 HTTP;高频进度走长连接。代价是客户端要维护 prompt ID、session ID 和重连后的状态对齐。

调试检查表

  1. POST /prompt 是否返回 prompt ID,而不是 validation error?
  2. /queue 中任务是在 waiting 还是 running?
  3. WebSocket 的 clientId 是否与 Prompt 的 client_id 一致?
  4. 是否收到 execution_start,但没有 executing?这通常已进入执行器。
  5. 是否只有预览丢失?检查 binary event 与 feature flags,不要先怀疑采样。
  6. 断开重连后是否用原 sid?新 sid 不会自动继承旧定向消息。

下一章会把 /prompt 里的 JSON 拆开,解释为什么 UI Workflow 不能原样交给执行器。

Last updated on