首页
/ DeerFlow 终端工作台 TUI 完全指南:安装、使用与架构源码解析

DeerFlow 终端工作台 TUI 完全指南:安装、使用与架构源码解析

2026-09-06 18:14:11作者:俞予舒Fleming

deerflow 是 DeerFlow 原生长时程 SuperAgent 框架(harness)附带的终端原生工作台:它以嵌入式方式直接运行在 DeerFlowClient 之上,无需拉起 Gateway、前端、nginx 或 Docker 服务,同时完整复用 DeerFlow 统一的 config.yaml、checkpointer、skills、memory、MCP 与 sandbox 配置。本文以 backend/docs/TUI.md 为骨架,结合 tui 包源码 与其单元测试,系统讲解它的安装启动、全部交互模式、界面布局、命令体系、分层架构,以及"TUI 会话出现在 Web UI 侧边栏"背后的共享持久化设计。

DeerFlow TUI 主界面预览

TUI 的设计定位:嵌入式 UI Shell,而非另一套 Agent

TUI 模块位于 backend/packages/harness/deerflow/tui/,核心定位在模块自述中写得非常明确:它是架设在既有嵌入式 harness 之上的 UI 外壳(UI shell),不会 fork 出任何不同的 Agent 行为。它只负责交互展示与命令解析,真正的对话、工具调用、记忆读写仍然由 DeerFlowClient 完成。

由此带来三个显著特性:

  1. 零服务依赖:运行 TUI 不需要 Gateway 进程、不需要前端构建产物、不需要 nginx 或 Docker Compose;
  2. 配置全对齐:TUI 会话与 Web UI 共享同一套 config.yaml、checkpointer(检查点)、skills、memory、MCP 与 sandbox 配置,你在终端里使用的就是 DeerFlow 的完整能力集;
  3. 按需轻量安装:TUI 以可选 extra 形式发布,核心 harness 安装保持精简,不强制引入 textual 等终端渲染依赖。

安装与启动

安装可选依赖

textual 是 TUI 的唯一渲染依赖,被声明为可选的 extra(pyproject.toml 中可见 tui = ["textual>=0.80"],同时 deerflow 控制台脚本 deerflow = "deerflow.tui.cli:main" 指向 TUI 的入口):

uv pip install 'deerflow-harness[tui]'
# 等价做法:仅安装 textual
pip install textual

值得一提的实现细节:textual 缺失时,deerflow 控制台脚本并不会崩溃。入口代码 cli.py 对 Textual App 采用懒加载——只有在真正需要拉起 UI 时才 from deerflow.tui.app import run_tui,若捕获到 ModuleNotFoundError 且缺失模块名为 textual,则打印清晰的安装提示并按模式优雅降级(强制 --tui 时返回退出码 1,否则回落到 headless 帮助并返回 0)。

启动模式一览

命令 行为
deerflow 当 stdin/stdout 均为 TTY 时启动 TUI
deerflow --tui 强制启动 TUI(缺少 textual 时给出清晰的诊断信息)
deerflow --tui-transparent 使用终端默认背景渲染 TUI
deerflow --cli 单次调用强制 headless/classic 模式
deerflow chat 与默认界面相同的 TUI 会话入口
deerflow --continue 恢复最近的线程
deerflow --resume THREAD 按 id 或标题恢复指定线程
deerflow --print "question" Headless 一次性问答,结果输出到 stdout
deerflow --json "question" Headless 流式输出,按行输出 StreamEvent JSON
deerflow --recursion-limit 250 --print "question" 设置 headless Agent 循环的 super-step 上限
echo "q" | deerflow --print 从 stdin 读取消息
DEER_FLOW_TUI=1 deerflow 通过环境变量强制启用 TUI
DEER_FLOW_TUI_TRANSPARENT=1 deerflow 通过环境变量持久化终端背景渲染

启动决策逻辑(源码视角)

上面这张表格背后是一段纯函数决策逻辑plan_launch() 接收 argv、stdin_isatty/stdout_isatty 与 env,返回一个 LaunchPlan(mode 为 "tui""print""json""headless-help" 之一),该函数不产生任何 I/O,因此可以纯粹地被单元测试覆盖(见 cli.py)。其关键判定顺序为:

  1. 优先处理 --print / --json:带参则直接进入对应 headless 模式;缺参且 stdin 是 TTY 时会提示"需要 MESSAGE 或管道 stdin";
  2. 处理 --cli:带位置参数时等价于一次 --print,否则在管道输入或 --continue 场景下继续 headless,仍无消息则输出帮助;
  3. 判定 TUI:满足 --tui、环境变量 DEER_FLOW_TUI 为真、或 stdin/stdout 同时是 TTY 三者之一时进入 TUI 模式;
  4. 兜底:以上都不满足(无 TTY 且未给 headless 参数)时返回 headless-help 并打印引导信息,而不是挂起等待。此时若携带具体原因,退出码为 2,纯提示则为 0。

如果无 TTY 可用且未指定 headless 参数,deerflow 会打印引导提示而不是挂住——这正是终端工具在管道、CI、脚本中应有的健壮行为。另外,--recursion-limit 只能与 --print--json--cli 搭配使用(源码中用 parser.error(...) 强制约束),且参数值必须为正整数(由 _positive_int 校验)。

main() 还会在入口处识别 deerflow extensions 子命令并转发给 deerflow.extensions.cli,用于安装和管理受信任的 Python 扩展。

透明背景渲染(Transparent Mode)

透明渲染是显式 opt-in 的,实色 DeerFlow 调色板依然是默认主题。透明模式对主屏、header、transcript、status、palette、composer 与模态浮层统一使用 Textual 的 ansi_default 背景,同时保留 truecolor 前景色与选中高亮,从而让 TUI 无缝融入使用自定义背景的终端。

命令行与环境的组合规则:

  • --tui-transparent:本次调用启用终端默认背景;
  • DEER_FLOW_TUI_TRANSPARENT=1:通过环境变量启用(二者在 plan_launch 中按"命令行优先、环境变量兜底"的方式合并,见 cli.py);
  • 组合 --tui-transparent--tui:当 UI 需要在未检测到 TTY 的场景下也被强制拉起时(例如某些终端包装器),两者搭配使用。

Headless 模式与递归上限(Recursion Limit)

Headless 单发模式非常适合脚本化调用、自动化验证与快速问答:

# 一次性问答,输出最终回答
deerflow --print "用中文总结 DeerFlow 的架构"

# 流式输出事件(newline-delimited JSON)
deerflow --json "列出当前可用工具"

# 从 stdin 读入消息
echo "hello" | deerflow --print

# 指定 super-step 上限的长任务
deerflow --recursion-limit 250 --print "调研并编写一份报告"

关于 --recursion-limit 需要理解两个边界:

  • Headless 运行的默认递归上限是 100。当预期 Agent 循环更长时,应显式传入更大的正值;
  • 这是 LangGraph 的 super-step 预算,一次"super-step"可以同时包含模型推理与工具执行步骤,并不与用户对话轮数一一对应。因此单轮多工具的长任务会快速消耗该预算;
  • 它与 Gateway 配置中的 max_recursion_limit 不是一回事:后者是 Gateway 对客户端上报值的安全上限(safety ceiling),而受信任的嵌入式 CLI 运行默认不使用它。

--json 模式下,源码 cli.py 会逐事件调用 session.client.stream(...),将每个 StreamEvent 序列化为 {"type": ..., "data": ...} 一行写往 stdout 并立即 flush,方便下游按行消费。headless 单发(--print)则直接调用 client.chat() 并打印最终答案。

会话恢复语义:--continue 与 --resume

--continue--resume 不只在 UI 中可用,也贯穿 headless 与 TUI。线程解析发生在 Session.resolve_thread()session.py):

  • --continue:拉取最近一个线程(list_threads(limit=1))并恢复;
  • --resume THREAD:接受线程 id 或标题两种引用——先按 id 精确匹配,再按标题精确匹配,两者都未命中时把原值当作字面 id 使用,前提是它满足规范线程 id 契约(1-64 位 ASCII 字母、数字、连字符或下划线,由 validate_thread_id 校验);
  • open_session() 通过 get_checkpointer()DeerFlowClient(checkpointer=...) 构建嵌入式会话;headless 单发路径传 persistence=False,避免为了一个用不到的特性去创建后台事件循环与连接池。

界面总览(Surface)

  • Header(顶部栏)——展示模型、当前线程、项目根目录、skill/tool 计数。
  • Transcript(对话区)——依次呈现用户输入、助手回答与紧凑的工具卡片。工具卡片形如 ⚙ Read path ✓,并附有暗色(dimmed)的结果预览。已完成(finalized)的助手消息按 Markdown 渲染(标题、加粗、列表、代码、链接均可);正在流式输出的消息保持纯文本,以避免频繁重排造成的跳动,一旦输出完成即刻"snap"成 Markdown。Transcript 重渲染被合并节流到约 16 fps,因此长线程流式输出依旧顺滑。
  • Status line(状态行)——运行状态 + 动画 spinner、模型、线程标题、token 用量,以及运行期间显示的 esc interrupt 提示。
  • Composer(输入框)——圆角输入框,按 / 即可打开命令面板。

快捷键

按键 动作
Enter 发送消息 / 确认面板选中项
/ 打开斜杠命令面板
/ 面板内导航;面板关闭时切换输入历史
PageUp / PageDown 滚动对话区(不移动焦点,保持输入框可继续输入)
Tab 补全高亮命令(自动追加一个尾随空格)
Esc 关闭面板 / 浮层
Ctrl+C 中断正在运行的请求;空闲时退出
Ctrl+L 重绘画面 · Ctrl+U 清空输入框

滚动行为细节:当视图停留在底部时,Transcript 会跟随新输出自动滚动;一旦你向上翻阅过,流式刷新会保持当前阅读位置,直到你按 PageDown 回到底部才恢复跟随。

斜杠命令(Slash Commands)

命令面板支持以下内置命令:

/help   /new   /clear   /goal   /threads (/switch)
/model  /skills  /tools  /mcp  /memory  /uploads  /usage  /config  /quit
  • 另外支持 /<skill-name> task:在当前回合激活任意已启用的 skill,语义与 DeerFlow 其他入口完全一致;
  • /model/threads 会打开模态选择器
  • /clear 只清空终端里当前的 transcript 行(纯显示层面),活动线程与已持久化的对话记录不受影响;并且在一次运行进行中,/new/clear 会提示"等待运行结束",而不会重置正在进行的显示状态(可对照 app.py 中 idle 态 ClearRows 与"Still working"提示的实现语义);
  • /goal 的目标管理三态/goal <condition> 设置当前线程目标,/goal 显示目标,/goal clear 清除目标。

架构:一层 UI 外壳背后的分层设计

原文给出了一幅"文件即职责"的架构速览,逐行对照源码模块如下:

cli.py            启动模式决策(纯函数)+ headless print/json + 程序入口
session.py        构建 DeerFlowClient(含 checkpointer)与持久化写入器
runtime.py        StreamEvent -> reducer actions(纯翻译层 + 线程驱动)
view_state.py     ViewState + reduce(state, action)(纯函数,可测试的核心)
message_format    紧凑工具摘要 / 截断(纯函数)
command_registry  斜杠命令注册表 + resolve(纯函数)
input_history     有界的 ↑/↓ 历史(纯函数)
render.py         Header / transcript / status / palette 的 Rich 渲染器(纯函数)
theme.py          调色板与符号
app.py            Textual App:组合 widgets,在工作线程驱动运行,
                  将 action 回送到 UI 线程,渲染 ViewState
persistence.py    写入 threads_meta,使会话出现在 Web UI 中

DeerFlowClient.stream() 是一个同步生成器,这是理解整个 TUI 运行时设计的关键:同步生成器意味着它不能直接跑在 Textual 的异步事件循环里做 await,因此 App 通过 Textual worker 线程来驱动它,并将生成器 yield 出的每个 action 通过 call_from_thread 送回 UI 线程。

纯函数内核:ViewState 与 reducer

view_state.py 是"可测试的心脏"。它不依赖 Textual 或任何渲染逻辑,把可见会话建模为不可变的类型化行(UserRow / AssistantRow / ToolRow / SystemRow)加一组有限 action,对外只暴露一个纯函数 reduce(state, action) -> state

几个值得注意的边界处理:

  • 流式合并策略_merge_stream_text 按"内容包含关系"而非盲目拼接合并增量——新文本完全包含旧文本视为累积快照直接替换;旧文本包含新文本视为过期/较短重发予以保留;否则才是真正的增量追加。这能吸收断线重连后 values 快照重放历史等场景;
  • 跨轮次去重:真实 message id 会跨整个 transcript 匹配(长线程上旧消息可能比新消息更晚被重发);而空 id("")不可作为全局匹配键——它会被每一轮的无 id 分片共享,若全局扫描会把新一轮回答折叠进旧行,因此匿名行改用"位置索引"(streaming_anonymous_row_index)追踪本轮目标行,并在每轮 RunStarted/RunEnded/ClearRows 时复位;
  • 工具卡片按 tool_call_id 去重:流式工具调用以多个 chunk 到达(先名字后增长的参数),无 id 的片段属于参数噪音直接丢弃;结果返回后卡片状态从 running 流转为 ok / error,即使起始 chunk 丢失也会把结果兜底渲染成一张新卡片;
  • Markdown 渲染的判定:只有 streaming_id(或匿名行索引)指向的那一行在流式期间保持纯文本,历史行一律按 Markdown 渲染,避免持续重排。

runtime.py:事件翻译层

runtime.py 负责把 DeerFlowClientStreamEvent 翻译成 reducer action(同样保持纯函数):

  • messages-tuple 中的 ai 消息 -> AssistantDelta(取文本)+ 每个工具调用 -> ToolStarted
  • tool 消息 -> ToolResult(依据 is_error / status == "error" 标记错误);
  • end 事件 -> RunEnded(usage=...),把 token 用量携带进状态;
  • values 事件中非空的 title -> ThreadTitle,用于同步线程标题。

外层 stream_actions() 则以 RunStarted … RunEnded 把整轮运行括起来,并把模型异常转换成一行 AssistantError 而不是让 UI 崩溃。

app.py:Textual 装配层

app.py 是唯一依赖 Textual 的层:它组合各 widget、以 run_worker 在工作线程执行 _stream_worker(内部逐个调用 call_from_thread(self._on_action, action) 把 action 送返 UI 线程),并负责命令面板、/goal 管理、model/thread 模态选择器、空闲态 /clear 路由、活动运行期间对状态重置类本地命令的拦截,以及通过 check_action 门控优先级按键绑定,避免抢占浮层或输入框的按键。

正因"除 app.py 外的纯层完全没有 Textual 依赖",它们才能被普通 pytest 直接使用合成 StreamEvent 单元测试,而不需要任何终端环境。

为何选择这种架构?

这套"UI 壳 + 纯 reducer 内核 + 工作线程桥接"的设计收益是清晰可测的:所有有业务价值的逻辑(流式增量合并、工具卡片、错误行、跨轮去重)都落在纯函数层,与终端渲染彻底解耦;app.py 只负责把 UI 事件翻译成 action、把 reducer 结果渲染成 widget。这使 TUI 的行为正确性可以在不启动真实模型、不碰终端的情况下被大规模回归验证(详见下文"测试"一节)。

Web UI 可见性:共享持久化的闭环

这是一个容易被忽视但非常关键的工程细节:Web UI 的会话列表读的是 threads_meta SQL 表(按 user_id 过滤),而不是 checkpointer。而嵌入式运行只写 checkpointer,因此如果 TUI 不额外做点什么,它的线程将永远无法出现在 Web UI 的侧边栏里。

persistence.py源码)正是为弥合这一缺口而存在:

  • 在线程首轮对话时,写入一行 threads_meta属主为本地默认用户 "default",写入的是 Gateway 读取的同一个数据库(源码中经 deerflow.persistence.engine.init_engine_from_config 构建共享 store,metadata 标记为 {"source": "tui"}),随后再同步自动生成的标题(update_display_name);
  • 只需要共享的 threads_meta store,并不需要 Gateway 进程本身在运行
  • 当数据库后端为 memory(无 SQL store)时,写入器静默降级为 no-op,TUI 依旧正常工作——整个持久化是 best-effort 的,所有写路径都吞掉异常,"可见性只是便利,绝不能成为打断会话的理由";
  • 所有 DB 工作运行在一个长驻的后台事件循环线程上(_LoopThread):因为 SQLAlchemy async engine 绑定在创建它的那个 loop 上,如果每次调用都 asyncio.run 新建 loop,连接会被绑定到一次性的事件循环上产生隐患。

测试体系

TUI 的测试遵循"纯层用 pytest 直测、UI 用 pilot harness 驱动"的分工,全部集中在 backend/teststest_tui_*.py 系列(共 16 个文件):

  • 纯层单测test_tui_cli.pytest_tui_runtime.pytest_tui_view_state.pytest_tui_message_format.pytest_tui_command_registry.pytest_tui_input_history.pytest_tui_render.pytest_tui_palette.pytest_tui_palette_render.pytest_tui_transparent.pytest_tui_session.py 等——直接喂入合成 StreamEvent / action,验证 reducer 与翻译逻辑;
  • UI 层测试test_tui_app.pytest_tui_overlays.pytest_tui_composer.py 通过 Textual 的 pilot harness 驱动 App,使用**假的进程内 session(不触碰真实模型)**来演练斜杠面板与模态浮层;
  • 持久化闭环test_tui_persistence.py 证明 threads_meta 写入/读取的完整往返。

本地运行全部 TUI 相关测试:

cd backend && PYTHONPATH=. uv run pytest tests/ -k tui -q

结语:何时选择 TUI

DeerFlow TUI 的价值在于把完整的 harness 能力(模型、线程、skills、MCP、memory、sandbox 与自动标题)搬进一个无服务依赖的纯终端会话:SSH 到服务器即可全功能使用,日志化调用走 --print / --json,交互式深度任务则进入完整 TUI,而共享的 threads_meta 持久化又保证它与 Web UI 侧边栏会话无缝衔接。需要进一步深入源码的读者,建议从三个入口继续:决策纯函数 cli.py 的 plan_launch、reducer 内核 view_state.py、以及 Web UI 可见性的 persistence.py;控制台脚本声明与依赖关系见 pyproject.toml

登录后查看全文
热门项目推荐
相关项目推荐