第 5 章 · 01 Rich 美化的 REPL 本节摘要:本节精读 的「门面」——一个用 库美化的交互式 REPL(读取-求值-打印循环)。从终端敲 进入后,先看到一屏双栏布局的欢迎面板(左侧 ASCII art + 版本,右侧 Tips + 最近任务),然后进入 提示符循环,接收用户输入。本节逐行拆 用到的三个 rich 组件—— (带边框面板)、 (多栏并排)、 (带样式的文本),看 函数如何把它们嵌套成 Claude Code 风格的双栏欢迎屏;再拆 的主循环与退出处理(EOF/Ctrl+C/quit)。读完本节,你理解终端 UI 的「门面」如何用几十行 rich 拼出来,以及它为何重要——CLI 是用户对框架的第一印象。
本节摘要:本节精读
autohedge/cli.py的「门面」——一个用rich库美化的交互式 REPL(读取-求值-打印循环)。从终端敲autohedge进入后,先看到一屏双栏布局的欢迎面板(左侧 ASCII art + 版本,右侧 Tips + 最近任务),然后进入>提示符循环,接收用户输入。本节逐行拆cli.py用到的三个 rich 组件——Panel(带边框面板)、Columns(多栏并排)、Text(带样式的文本),看_welcome()函数如何把它们嵌套成 Claude Code 风格的双栏欢迎屏;再拆run_repl()的主循环与退出处理(EOF/Ctrl+C/quit)。读完本节,你理解终端 UI 的「门面」如何用几十行 rich 拼出来,以及它为何重要——CLI 是用户对框架的第一印象。
内容来源:原项目源码
autohedge/cli.py(约 230 行),精读并套用体系化模板。
阅读完本节,你应当能够:
cli.py 的 import 段与模块级常量。_welcome() 如何嵌套出双栏欢迎屏。run_repl() 的主循环结构与退出处理。cli.py 第 1-30 行:
import argparse import sys from pathlib import Path from rich.console import Console from rich.panel import Panel from rich.text import Text from rich.columns import Columns from rich import box from autohedge.env_loader import load_env, require_openai_key load_env() if not require_openai_key(): Console().print( "[yellow]Warning: OPENAI_API_KEY not set. Set it in .env or export it.[/]" ) try: from importlib.metadata import version as _version VERSION = _version("autohedge") except Exception: VERSION = "0.1.2" console = Console()
逐段解读:
argparse(命令行解析,第 2 节讲)、sys(读 argv、退出)、pathlib.Path(跨平台路径)。Console(输出引擎)、Panel(带边框的面板)、Text(带样式的文本)、Columns(多栏并排)、box(边框样式枚举)。load_env, require_openai_key(第 2 章讲过的 .env 加载与 key 校验)。load_env() if not require_openai_key(): Console().print("[yellow]Warning: OPENAI_API_KEY not set. ...[/]")
模块加载时就调 load_env(),然后校验 OpenAI key——缺了只警告不退出(用 yellow 警告色)。注意这里用 Console().print(...)(临时 Console),因为下面的全局 console = Console() 还没定义。
try: from importlib.metadata import version as _version VERSION = _version("autohedge") except Exception: VERSION = "0.1.2"
用 importlib.metadata.version 从已安装的包元数据取版本(pyproject.toml 里的 version = "0.1.5")。取不到(比如没 pip install -e . 直接跑源码)就回退到硬编码的 "0.1.2"——注意回退值与 pyproject 的 0.1.5 不一致,这是又一处小瑕疵。
_welcome() 用这三个组件拼欢迎屏,先认识它们:
Text("Welcome to AutoHedge", style="bold orange1") 创建一段文本,style 控制样式(粗体 + 橘色)。Text 对象支持 .append(other_text) 拼接不同样式的片段——一段 Text 可以含多种样式。
Panel(content, box=box.MINIMAL, padding=(0, 1), border_style="dim", expand=False)
把任意内容(文本、其他 Panel)包进一个带边框的盒子。参数:
box=box.MINIMAL:边框样式(极简,只有顶底线)。padding=(0, 1):上下 0、左右 1 字符的内边距。border_style="dim":边框颜色(暗淡灰)。expand=False:不撑满终端宽度(按内容自适应)。Columns([panel_a, panel_b], expand=True, equal=False)
把多个组件横向并排成几栏。expand=True 让栏撑满宽度,equal=False 允许栏宽不等。
💡 核心心法:rich 的设计哲学是「组件可嵌套」——Text 里能放 Text、Panel 里能放 Panel、Columns 里能放 Panel。用嵌套就能拼出复杂布局,像搭积木。AutoHedge 的欢迎屏就是「外层 Panel 包 Columns,Columns 里再放两个 Panel」的三层嵌套。
cli.py 第 75-141 行的 _welcome(),逻辑分四步:
cwd = Path.cwd() try: cwd_str = cwd.relative_to(Path.home()) cwd_display = f"~/{cwd_str}" except ValueError: cwd_display = str(cwd)
把当前目录转成 ~/相对 home 的路径形式(像 ~/projects/autohedge)。如果不在 home 下(relative_to 抛 ValueError),就回退成绝对路径。这是 shell 风格的路径简写。
左侧 Text(标题 + ASCII art + 版本):
welcome = Text("Welcome to AutoHedge", style="bold orange1") subtitle = Text(f"v{VERSION} · {cwd_display}", style="dim") ... left = Text() left.append(welcome) left.append("\n\n") left.append(BANNER_ART, style="cyan") left.append("\n") left.append(subtitle)
BANNER_ART 是模块级 ASCII art(一个抽象的图表/对冲符号,第 33-39 行)。
右侧 Text(Tips + 最近任务):
tips_text = Text("Tips for getting started\n", style="bold orange1") tips_text.append(" — ".join(TIPS), style="dim") recent = _get_recent_tasks() recent_heading = Text("Recent activity\n", style="bold orange1") if recent: recent_body = Text("\n".join(recent[-3:]), style="dim") else: recent_body = Text("No recent activity", style="dim")
TIPS 是三条提示(模块级常量);_get_recent_tasks() 读历史(第 2 节专讲)。
left_panel = Panel(left, box=box.MINIMAL, padding=(0, 1), border_style="dim", expand=False) right_panel = Panel(right, box=box.MINIMAL, padding=(0, 1), border_style="dim", expand=True)
左右各包成一个极简 Panel。注意 expand 不同:左 False(按内容宽),右 True(撑满剩余宽)。
cols = Columns([left_panel, right_panel], expand=True, equal=False) console.print( Panel( cols, title=f"[bold]AutoHedge v{VERSION}[/]", title_align="left", border_style="cyan", padding=(0, 1), ) )
把两个 Panel 用 Columns 并排,再整体包进一个外层 Panel(青色边框、左对齐标题 AutoHedge v0.1.x)。最终屏幕上看到的是:一个青色大框,里面左右两栏——这是 Claude Code 风格的欢迎屏(注释第 114 行明说)。
cli.py 第 144-186 行的 run_repl(),REPL 的核心:
def run_repl() -> None: _welcome() while True: try: prompt = Text("> ", style="bold cyan") console.print(prompt, end="") line = input().strip() except (EOFError, KeyboardInterrupt): console.print("\n[dim]Goodbye.[/]") break if not line: continue lower = line.lower() if lower in ("quit", "exit", "q"): console.print("[dim]Goodbye.[/]") break if lower in ("help", "?", "h"): for t in TIPS: console.print(f" [dim]·[/] {t}") continue # Treat as task prompt task = line _append_recent(task) try: from autohedge import AutoHedge system = AutoHedge() console.print("[dim]Running...[/]") result = system.run(task=task) console.print(Panel(str(result)[:2000], title="Result", border_style="green")) except Exception as e: console.print(f"[red]Error: {e}[/]")
逻辑分段:
break 退出循环。这是 REPL 的标准礼貌退出。.lower()):正常退出。continue 跳过,重新提示。help/?/h:打印三条 TIPS(缩进 + 灰色 · 列表符)。
把非命令的输入当作任务,这是 REPL 的核心交互:
_append_recent(task):写进最近任务历史(第 2 节专讲)。from autohedge import AutoHedge 写在循环内而非文件顶部——刻意为之,让 cli.py 即使 AutoHedge 依赖未就绪也能显示欢迎屏。AutoHedge(),打「Running...」,调 run(task=task)。str(result)[:2000] 截断到 2000 字符(防止超长输出刷屏)。except Exception:红字打错误,不退出循环(用户可以继续输入下一条)。💡 核心心法:注意第 4 步的
[:2000]——它体现了「门面层做防御」的思路:CLI 是用户直接面对的层,不能因为某次输出太长就把屏幕刷爆。这种「在边界做截断/防御」是 CLI 设计的常识。
AutoHedge 核心是 Agent 编排,却花了 cli.py 近 230 行做 UI——为什么?因为:
autohedge 第一眼看到的就是欢迎屏。粗糙的 print 与精致的面板,感知差距巨大。autohedge 命令 ──► cli.py main()(第 2 节) ──► run_repl() │ _welcome()(本节) + 主循环(本节) │ 命令路由 / 历史(第 2 节) ──► AutoHedge.run(第 3 节)
load_env()、警告缺 OpenAI key(不退出);VERSION 从包元探测,回退 0.1.2(与 pyproject 0.1.5 不一致)。Text(带样式文本,可拼接)、Panel(带边框盒子,可嵌套)、Columns(横向并排);设计哲学是组件可嵌套,像搭积木。_welcome() 四步:算路径显示 → 构造左右 Text(左:标题+ASCII+版本;右:Tips+最近任务)→ 各自包 Panel → Columns 并排再套外层青色 Panel,成 Claude Code 风格双栏屏。run_repl() 主循环:_welcome() 后 while True;EOF/Ctrl+C/quit 三种退出;空行跳过;help 打 Tips;其余当任务执行。[:2000] 截断防刷屏;异常红字但不退出循环。下一节,我们看 cli.py 里的命令解析(argparse)与最近任务历史(
~/.autohedge/recent_tasks.txt的读写)。