03 · 服务、队列与传输
PromptServer 看起来像普通 aiohttp 应用,实际承担了四个角色:静态前端宿主、控制 API、实时消息总线、执行器与外部世界之间的适配层。理解它的关键是把 HTTP 请求、队列状态、工作线程、WebSocket 推送 分成四段。
外部协议总览
中间件先建立安全与兼容边界
PromptServer 构造时按配置组合中间件:
- cache-control:避免动态 API 被错误缓存;
- deprecated route 提示;
- 可选响应体压缩;
- 显式 CORS,或默认的 loopback origin 防护;
- 禁用 API Nodes 时附加 CSP;
- Manager 相关中间件。
默认为什么检查 Host 与 Origin
服务常监听 127.0.0.1,但“只监听本机”不等于浏览器请求天然安全。恶意网页可以尝试让用户浏览器请求本地端口。默认中间件会比较 loopback Host 与 Origin 的主机名,阻止跨站来源利用本机 ComfyUI。
显式配置 CORS 是另一种策略,意味着操作者主动声明允许的来源。两者不能当作同一个开关的正反面:宽泛 CORS 会扩大控制 API 的可调用面。
路由不是一张平铺表
可以按资源性质分为六组:
| 组 | 代表路由 | 状态来源 |
|---|---|---|
| 能力发现 | /system_stats、/features、/object_info | 运行时、节点映射 |
| 模型与文件 | /models、/upload/image、/view | folder_paths、磁盘 |
| 执行控制 | /prompt、/queue、/interrupt、/free | PromptQueue、model management |
| 结果查询 | /history、/jobs | 内存 history / job facade |
| 用户与内部服务 | /userdata、/settings、/internal/... | DB、文件服务、日志 |
| 可选扩展 | /api/...、Assets、模板、subgraphs | Managers、feature flags |
add_routes() 还把部分旧路由复制到 /api 前缀,挂载内部子应用、扩展 Web 目录、模板与嵌入式文档,最后才托管前端静态根目录。
这解释了一个常见误会:本仓库并不包含完整前端开发源码,运行时前端由 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:
- 若 ID 已存在,旧连接被替换,可用于重连;
- 若没有,服务端生成 session ID;
- 服务立即发送当前队列状态;
- 双方协商 feature flags / socket metadata;
- 连接保存在
sockets[sid],断开时清理当前实例。
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、ExecutionBlocker | WebSocket execution_error + history status |
| 传输 | Socket 已断开 | 清理连接;执行可继续 |
| 文件服务 | 路径越界、危险类型 | 拒绝或强制下载 |
Socket 断开不自动等于取消 Prompt。执行任务属于队列,不属于 WebSocket 对象;这是 API 客户端可重连查询结果的基础。
设计取舍与边界
单 worker 的好处
模型复用、显存状态和节点副作用更易推理,不会让两个采样任务无意争抢同一套 patcher。代价是实例内吞吐依赖批处理或外部横向扩容,而非简单并发多个 Prompt。
内存队列的好处
低延迟、实现简单、可直接携带 Python 对象。代价是不耐进程故障、不能跨实例协调。生产任务系统应把 ComfyUI 当执行后端,在外层做 durable orchestration。
HTTP + WebSocket 的好处
提交、查询与资源传输留在可缓存/可重试的 HTTP;高频进度走长连接。代价是客户端要维护 prompt ID、session ID 和重连后的状态对齐。
调试检查表
POST /prompt是否返回 prompt ID,而不是 validation error?/queue中任务是在 waiting 还是 running?- WebSocket 的
clientId是否与 Prompt 的client_id一致? - 是否收到
execution_start,但没有executing?这通常已进入执行器。 - 是否只有预览丢失?检查 binary event 与 feature flags,不要先怀疑采样。
- 断开重连后是否用原 sid?新 sid 不会自动继承旧定向消息。
下一章会把 /prompt 里的 JSON 拆开,解释为什么 UI Workflow 不能原样交给执行器。