DeerFlow 终端工作台 TUI 完全指南:安装、使用与架构源码解析
deerflow 是 DeerFlow 原生长时程 SuperAgent 框架(harness)附带的终端原生工作台:它以嵌入式方式直接运行在 DeerFlowClient 之上,无需拉起 Gateway、前端、nginx 或 Docker 服务,同时完整复用 DeerFlow 统一的 config.yaml、checkpointer、skills、memory、MCP 与 sandbox 配置。本文以 backend/docs/TUI.md 为骨架,结合 tui 包源码 与其单元测试,系统讲解它的安装启动、全部交互模式、界面布局、命令体系、分层架构,以及"TUI 会话出现在 Web UI 侧边栏"背后的共享持久化设计。
TUI 的设计定位:嵌入式 UI Shell,而非另一套 Agent
TUI 模块位于 backend/packages/harness/deerflow/tui/,核心定位在模块自述中写得非常明确:它是架设在既有嵌入式 harness 之上的 UI 外壳(UI shell),不会 fork 出任何不同的 Agent 行为。它只负责交互展示与命令解析,真正的对话、工具调用、记忆读写仍然由 DeerFlowClient 完成。
由此带来三个显著特性:
- 零服务依赖:运行 TUI 不需要 Gateway 进程、不需要前端构建产物、不需要 nginx 或 Docker Compose;
- 配置全对齐:TUI 会话与 Web UI 共享同一套
config.yaml、checkpointer(检查点)、skills、memory、MCP 与 sandbox 配置,你在终端里使用的就是 DeerFlow 的完整能力集; - 按需轻量安装: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)。其关键判定顺序为:
- 优先处理
--print/--json:带参则直接进入对应 headless 模式;缺参且 stdin 是 TTY 时会提示"需要 MESSAGE 或管道 stdin"; - 处理
--cli:带位置参数时等价于一次--print,否则在管道输入或--continue场景下继续 headless,仍无消息则输出帮助; - 判定 TUI:满足
--tui、环境变量DEER_FLOW_TUI为真、或 stdin/stdout 同时是 TTY 三者之一时进入 TUI 模式; - 兜底:以上都不满足(无 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 负责把 DeerFlowClient 的 StreamEvent 翻译成 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_metastore,并不需要 Gateway 进程本身在运行; - 当数据库后端为
memory(无 SQL store)时,写入器静默降级为 no-op,TUI 依旧正常工作——整个持久化是 best-effort 的,所有写路径都吞掉异常,"可见性只是便利,绝不能成为打断会话的理由"; - 所有 DB 工作运行在一个长驻的后台事件循环线程上(
_LoopThread):因为 SQLAlchemy async engine 绑定在创建它的那个 loop 上,如果每次调用都asyncio.run新建 loop,连接会被绑定到一次性的事件循环上产生隐患。
测试体系
TUI 的测试遵循"纯层用 pytest 直测、UI 用 pilot harness 驱动"的分工,全部集中在 backend/tests 的 test_tui_*.py 系列(共 16 个文件):
- 纯层单测:
test_tui_cli.py、test_tui_runtime.py、test_tui_view_state.py、test_tui_message_format.py、test_tui_command_registry.py、test_tui_input_history.py、test_tui_render.py、test_tui_palette.py、test_tui_palette_render.py、test_tui_transparent.py、test_tui_session.py等——直接喂入合成StreamEvent/ action,验证 reducer 与翻译逻辑; - UI 层测试:
test_tui_app.py、test_tui_overlays.py、test_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。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00