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_replrun_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-listoh 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循环中

写到最后

notion image
是在往前走就好 bothsavage.github.io
 
notion image
 
Prev
将进酒
Next
OpenHarness源码研究-1-配置打包管理
Loading...