数据截至 (上游 commit b78a3462c9a6)
一次运行的生命周期:从 HTTP 请求到 SSE 流
30 秒导读: 你在 Dify 画布上点一次「运行」,浏览器发出的是一个普通 HTTP POST,收回来的却是一条持续几十秒的 SSE 事件流。这中间 Dify 做了一件核心的事:把「执行」和「回话」拆到两个执行体上——执行体在后台跑图、往队列里丢事件,HTTP 这一侧只负责从队列里捞事件、翻译成
data: {...}往下吐。本章只讲这条骨架,不进节点内部。
1. 这章讲什么
一句话:用户点一次运行,后端从收到请求到吐完最后一个 SSE chunk,中间经过了哪些部件、按什么顺序。
读完你应该能回答四个问题:
| 问题 | 本章第几节 |
|---|---|
AppMode 的八个取值怎么分流?谁挡住并发? | §3 |
| 谁真正在跑工作流?为什么不能在 HTTP 线程里直接跑? | §4、§5、§6 |
| 「停止」按钮按下去,凭什么能把跑到一半的执行掐掉? | §7 |
引擎里的一个事件,怎么变成浏览器收到的一行 data:? | §8、§9 |
本章不覆盖(在兄弟章里):
- 画布数据怎么存、DSL 带走什么 → 02 编辑侧
- JSON 图怎么变成可执行节点、graphon 边界在哪 → 03 节点装配
- Layer 钩子、留痕落库、单节点调试 → 04 执行期横切
- 暂停等人、审批表单、恢复执行 → 05 human-in-the-loop
- 触发器入口与插件运行时 → 06 触发器与插件
2. 顶层全景:一次运行分成五段路
先给一张最粗的图。从上往下读,就是一次请求的时间顺序;左边一列是「回话」的路,右边一列是「执行」的路,中间那条竖线是两条路唯一的接触面——队列。
浏览器:POST .../workflows/draft/run
│
▼
① 入口分发 AppGenerateService.generate ← 挑模 式 + 限流 + 计费
│
▼
② 起摊子 XxxAppGenerator.generate/_generate
│
┌──────────────────┴──────────────────┐
│ │
(回话侧 / 调用方线程) (执行侧 / 工作线程或 Celery worker)
│ │
▼ ▼
③ 消费 GenerateTaskPipeline ③' 生产 XxxAppRunner.run()
.process() → WorkflowEntry → GraphEngine
│ │
│ ◄ ──────── AppQueueManager ─────────┤ 引擎事件 publish 进队列
│ (queue.Queue) │
▼ ▼
④ 翻译 Queue*Event → *StreamResponse 跑完发终止事件 → stop_listen()
│
▼
⑤ 出口 ResponseConverter → "data: {...}\n\n" → Flask SSE Response
五段路各归谁管:
| 段 | 干什么 | 主文件 |
|---|---|---|
| ① 入口分发 | 按 app 模式选 Generator,做并发限流与配额 | api/services/app_generate_service.py |
| ② 起摊子 | 组装运行实体、仓储、队列管理器,起执行体 | api/core/app/apps/*/app_generator.py |
| ③' 生产 | 建图、跑 GraphEngine、把引擎事件塞进队列 | api/core/app/apps/*/app_runner.py |
| ③ 消费 | 监听队列,逐事件产出流式响应对象 | api/core/app/apps/*/generate_task_pipeline.py |
| ⑤ 出口 | 响应对象 → dict → SSE 文本行 | api/core/app/apps/*/generate_response_converter.py |