OpenHarness源码研究-2-CLI构建工具Typer
type
Post
status
Published
date
Apr 11, 2026
slug
260411-openhasness-2
summary
从cli.py,用传统web开发的视角,看typer框架如何定义通信和交互的,以及观察命令是如何设计的
tags
开发
category
技术分享
icon
password
前文
从cli.py,用传统web开发的视角,看typer框架如何定义通信和交互的,以及观察命令是如何设计的
运行主方法
入口方法
openharness中__main__可以看到主方法就是cli中app,并且从toml中可以看到
oh脚本指定的代码在openharness.cli:app- typer.Typer:是一个命令行参数解析器,相当于命令行的Web框架,一个能像开发微服务一样开发命令行工具的利器,如果你写过FastAPI那么Typer就像是把路由从URL搬到了终端。它利用Python的类型提示(Type Hints),让你的CLI程序像写Web API一样优雅:自动参数转换、自动帮助文档、自动错误拦截 。它不再是原始的 sys.argv 字符串切割,而是一个现代化的、声明式的命令行框架
- help:
uv —-help
- add_completion:是否自动添加安装 shell 补全的命令。
- rich_markup_mode:是否开启 Rich 渲染支持(让报错和帮助更漂亮)。
- invoke_without_command
- 代理模式(True):像python,直接输入 python 进入交互环境。
- 命令模式(False):像docker直,接输入 docker 会报错,必须指定 docker run
根命令
根命令入参
- 装饰器
@app.callback:指定invoke_without_command=True,即使没有输入任何子命令(比如mcp, plugin等),也允许直接触发这个被@app.callback装饰的 main 函数,类比Web路由中的根路径/的get请求处理器
- main方法中的入参
typer.Context,这个就是此次会话的上下文对象,在SprintBoot中,它类似于HttpServletRequest,在FastAPI中,它类似于Request对象,作用如下: - 查看路由信息:通过ctx.invoked_subcommand知道用户到底访问了哪个“接口”(子命令),所以在main方法中
if ctx.invoked_subcommand is not None: return代表如果用户运行的是oh mcp(即invoked_subcommand为"mcp"),那么main函数就只负责解析参数,解析完就直接 return。如果用户只输入了oh,没有子命令才继续执行main下面的代码 - 共享数据:可以在main函数里往ctx.obj存东西,然后在子命令函数里取出来
version: bool = typer.Option- False:默认值
- "--version", "-v":命令行标签:长标签和短标签
- help="Show version and exit":帮助文档中的说明
oh —help中可以看到 - callback=_version_callback:就是简单的回调函数
- is_eager=True:优先级,最优先处理,不管后面还有啥参数
continue_session: bool = typer.Option- rich_help_panel="Session”:这纯粹是为了好看。就像Swagger里的@Tag或者是 API文档的分组。它会让你的
oh --help更好看
- 剩下的自己看吧
根命令方法体
- 在处理会话
asyncio.run之前的代码很清晰,没啥好说的
- 主要关心的是
asyncio.run中调用的run_repl和run_print_mode这两个异步方法,asyncio.run是 “同步进入异步” 唯一方法,所以这里不能用await
- 我们在运行最基本的程序
oh时,并没有指定比如cwd,model,max_turns等等参数,默认值为None,也会传递给run_repl函数,在函数内部会去自动读取环境变量或者配置文件~/.openharness/settings.json,这也是直接运行oh报错的原因,因为都没读取到
- run_repl中的repl就是所谓的 REPL (Read-Eval-Print Loop,读取-执行-打印-循环) 模式
- Read (读):程序停在input()或者prompt_toolkit的输入框,等待你打字
- Eval (算):你按下回车,程序prompt发给AI
- Print (印):AI 回答后,程序把文字打印出来
- Loop (回):代码里有一个while True 循环,会重新跳回到第一步,继续等输入
- 其他参数都很好懂,backend_only默认为false就是把命令行交互页面激活,为true的时候暂时不分析
单层子命令-setup
多层子命令-5个
定义
- 在构建大型 CLI 工具时,单一入口往往力不从心。OpenHarness 利用 Typer 的 add_typer 机制,实现了一套类似 FastAPI APIRouter 的分层路由体系。这不仅让代码结构清晰(登录归登录,配置归配置),更让用户获得了一套逻辑严密的‘动词+名词’式命令行交互体验
- 这里定义了5个子命令,mcp,plugin,auth,provider,cron,使用如
oh mcp执行
- 在app.add_typer这种挂载方式下,在主app定义的全局参数(如--debug),在运行子命令
oh --debug mcp list时依然生效
add_typer(mcp_app)方式比起@app.command("setup")方式来说更合适,因为一旦用了后者,就没法在mcp后面再跟别的操作了。如果你想实现oh mcp list和oh mcp add,就得写成oh mcp-list和oh mcp-add这种扁平的命令
MCP子命令
@mcp_app.command(”xxx")代表为mcp添加子命令,为什么不使用mcp_app.add_typer这种方式,是因为到这里就不会再有子命令了,这也是CLI的常见设计
- 就是简单的增删查操作,先暂时不展开到方法内部
- 通过
load_settings()和save_settings(),大概知道是先把本地配置读到内存,操作完成后再写回磁盘,大概是这个文件~/.openharness/settings.json
plugin子命令
- 插件的增删改查,同样没啥好说的,很清晰
- 这里的函数内部的
from openharness.plugins.installer import …是CLI 工具优化的常用手段,如果把所有包都写在文件顶部,每次运行oh --help都要加载成百上千个库,速度会非常慢
cron子命令
- cron定时任务管理系统,在当前层面也很清晰
- toggle就是“开关”或“切换”的意思,用于控制单个cron任务,比如有个任务叫bakcup,
oh cron toggle backup false代表关了这个任务
- start/stop是粗颗粒度的。它控制的是进程的生死。当需要升级代码或彻底停止调度时,才会用到
- start/stop是电源总闸,toggle是每个房间的电灯开关
auth子命令
- 这段代码实现了OpenHarness的认证子系统,负责管理API Key、OAuth令牌以及与不同 AI供应商的链接状态
- 交互式设计的体现:如果用户直接输入
oh auth login,会打印一个带数字编号的菜单,并使用typer.prompt等待输入,根据用户选择的provider,最终调用_login_provider,实现不同的供应商触发不同的登录流,有的输入key,有的跳浏览器
- copilot,codex和claude的认证方式各不相同,这里只是去拿他们的key,没啥好说的,先搁置,后面看有没有分析的必要
provier命令
- 有了auth后得到了一大堆模型,只是就可以在把这些模型管理起来,这里也没啥好说
总结
- 里面还有些内部调用方法就先不管了
- typer.Typer就是一个现代化的、声明式的命令行框架,如果自己写的命令行工具,这框架可以学习下
- 命令也要有工程化的思维,需要有分组的概念,利用命令的特性使用不同的声明方式
- Read-Eval-Print Loop,(读取-执行-打印-循环) 模式
- asyncio.run是 “同步进入异步” 唯一方法,所以这里不能用await,这个cli的核心就在REPL循环中
写到最后
Prev
将进酒
Next
OpenHarness源码研究-1-配置打包管理
Loading...

