如果你曾经把一个可用的 Beancount 环境交给同事、新笔记本电脑或夜间 cron 任务,你就知道记账从来不是最难的部分。难的是工具链:匹配的 Python 版本、路径上的 bean-check 和 bean-query、为一张资产负债表拉进来的报表库,以及一个当你一问问题就重写文件的格式化工具。bea 0.2.0 于 2026 年 9 月 12 日发布,用一次安装取代了那份清单。bea 命令现在携带完整的原生 Beancount 工具链,在它自己配置的托管引擎中运行,并保持脚本和 AI 代理已经依赖的机器可读契约。
这是 0.2.0 的发布说明,按照我们内部跟踪发布的方式撰写:发布了什么、底层有什么变化、在进入软件包索引之前如何验证、它刻意不做什么,以及如何升级。如果你想看第一次运行的说明,0.1.0 发布文章和 CLI 快速入门是更短的阅读材料。
发布概览
两个渠道发布同一个命令。选一个,然后确认它能用版本号应答:
$ brew install bex-co/tap/bea # macOS 和 Linuxbrew
$ uv tool install beancount-io # 任何有 uv 和 Python 3.12 或更新版本的地方
$ bea --version
bea 0.2.0cli-v0.2.02026-09-12beancount 3.2.3 beanquery 0.2.0beangulp 0.2.0 beanprice 2.1.03.12 3.140.2.0 发布卡片:标签和发布日期、托管引擎固定的 Beancount 和 Beanquery 版本、两个可选引擎功能,以及发布时安装并测试过的 Python 版本。
| 项目 | 内容 |
|---|---|
| 版本 | 0.2.0,标签 cli-v0.2.0,2026-09-12 发布到 PyPI 和 bex-co/homebrew-tap Homebrew tap |
| 上一版本 | 0.1.0,2026-09-09 打标签,早三天 |
| 变更集 | 27 次提交涉及 CLI,119 个文件变更,约新增 12,300 行、删除 2,100 行 |
| 引擎固定 | 基础引擎中 Beancount 3.2.3 和 Beanquery 0.2.0;Beangulp 0.2.0 和 Beanprice 2.1.0 作为可选功能 |
| 头条 | 所有原生 Beancount 工具都在一个前缀下,由托管引擎提供;0.1.0 的 JSON 封装和退出码契约不变 |
底层变化:托管引擎
在 0.1.0 中,bea 像任何 Python 工具一样,把 Beancount 导入到自己的进程中。这可行,但让 CLI 的依赖图变成了 Beancount 的依赖图,并把"先安装 Beancount"留成了每个指南中未写明的步骤。
0.2.0 在程序中间画了一条线。bea 前端,即拥有命令、选项和渲染的部分,从不加载 Beancount、Beanquery 或内置的 Fava 报表代码。本地账本工作在托管引擎中运行:这是一个独立的 Python 环境,bea 从哈希固定的锁文件中配置并作为子解释器启动。前端跨该边界发送 JSON 请求并渲染返回的内容。你不需要安装 Beancount、把 bean-* 工具放到路径上,或考虑它们找到的是哪个 Python。
引擎的获取方式取决于渠道:
- Homebrew 在安装期间创建前端和引擎环境。本地命令使用 keg 本地引擎,无需额外下载。
- PyPI(
uv tool install或 pipx)在首次使用时配置。第一个需要引擎的本地命令会下载固定的组合,这需要网络访问和路径上的uv一次。后续命令离线复用~/.local/share/bea/engine/<version>,如果你设置了XDG_DATA_HOME,则在该目录下。
这个设计带来三个特性,每个都消除了一张我们已经见过的支持工单:
- 升级保持配对。
bea upgrade把更新交给安装此副本的包管理器,然后重建匹配的引擎,因此前端和引擎永远不会漂移到不同版本。 - 损坏的引擎自我修复。 如果配置中途失败,托管环境会被丢弃,并在下次成功尝试时重建。路径上其他地方的杂散
bean-check二进制文件会被忽略,不会被意外拾取。 - 重量级可选组件保持可选。 Beangulp 导入框架需要系统的
libmagic库,Beanprice 引入报价获取依赖。两者都不在基础引擎中。你显式启用它们,并且只在引擎中启用。
$ bea engine status
$ bea engine enable beangulp # 导入辅助;需要系统 libmagic 库
$ bea engine enable beanprice # bean-price 报价获取bea engine status 报告引擎是否已配置以及启用了哪些可选功能,不需要网络即可报告。如果首次配置失败,修复网络或 uv 后重新运行任何本地命令,例如 bea check。不要在旁边 pip install beancount:前端不会使用它。
每个原生工具,一个前缀
引擎是机制。面向用户的改变是对齐:上游 Beancount 项目发布的每个可执行文件现在都有一个 bea 对应命令,转发相同的参数并保留相同的输出。
$ bea check # bean-check,加上 bea 的 --json 封装
$ bea format main.bean -o clean.bean # bean-format:默认输出到 stdout,-i 重写
$ bea query "SELECT account, sum(position) GROUP BY account"
$ bea doctor context main.bean 2026-01-02 # 全部十一个 bean-doctor 操作
$ bea example --seed 1 -o example.beancount # bean-example
$ bea treeify < balances.txt # treeify
$ bea ingest identify --config ingest.py inbox # Beangulp,在引擎启用后
$ bea price -e USD:yahoo/AAPL # bean-price,在引擎启用后bean-checkbea checkbean-formatbea formatbean-querybea querybean-doctorbea doctorbean-examplebea exampletreeifybea treeifybeangulpbea ingest bea engine enable beangulpbean-pricebea price bea engine enable beanprice对齐映射:虚线以上六个原生 Beancount 可执行文件开箱即用;虚线以下两个在你启用引擎中的该功能后转发到 Beangulp 和 Beanprice。
其中一些值得比表格中的一行更多说明。
bea check 就是 bean-check,叠加了 bea 的 JSON 封装:相同的验证、相同的错误消息,在 --json 下包含脚本已经解析的相同 valid 和 errors 字段。
bea format 改变了行为,这是本版本中唯一可能让脚本惊讶的变化。在 0.1.0 中,bea format PATH 会重写文件。现在它把格式化后的文本打印到 stdout,并保持文件不变。--in-place (-i) 才是重写,--output FILE (-o) 写到其他地方,--check 是 CI 门禁,在需要格式化时以退出码 1 退出,--dry-run 列出会变化的内容。这遵循 bean-format,其默认值是安全的:读取路径并静默重写的命令无法先试运行。格式化是文本转换,不是解析,所以它不再拒绝有语法错误的文件;它对齐识别到的部分并保留其余部分。使用 bea check 检查有效性。
bea query 扩展了完整的原生表面。 它接受 BQL 作为参数、从 stdin 或交互式 shell 输入,交互式 shell 现在是上游 Beanquery shell,作为子进程启动,保留其 .format、.output、.run 和 .set 命令。--format 选择 text、csv 或 beancount 渲染,--numberify 把金额拆成每币种一列,-o 写入文件,--source URI 直接传递原生 Beanquery 源。
bea doctor 暴露全部十一个 bean-doctor 操作:lex、parse、roundtrip、directories、list-options、print-options、context、linked、region、missing-open 和 display-context。如果你曾经用 bean-doctor context 调试过记账问题,它就是同一工具,在同一个地址。
bea example 和 bea treeify 是原生生成器和原生树渲染器,原样转发。
bea ingest 和 bea price 在 bea engine enable 后分别转发到 Beangulp 的 identify、extract 和 archive 以及 bean-price。无 Python 的 CSV 路径 bea import --csv 两者都不需要,保持不变。
一个规则把转发的命令联系在一起:doctor、example、treeify、price 和 ingest 原样传递它们的参数给上游,并保留上游的输出和退出状态。这也意味着它们把账本作为自己的位置参数,如 bea doctor lex main.bean,而不是通过全局 --file。下面的封装和退出码类别描述 bea 自己的命令。
脚本可以继续信任的契约
机器可读的表面没有任何变动。全局 --json 仍然在 stdout 上放一个封装,包含 bea、target、data 和 truncated,以及在有限列表上附加 limit,在分页托管列表上附加 page。金额是十进制字符串,绝不是浮点数,日期是 ISO YYYY-MM-DD。--json 隐含 --no-input;非终端 stdin 或真值 CI 变量也是如此,所以无人值守任务永远不会等待人类。--strict 即使在终端中也拒绝部分答案,每个读取命令的 --allow-errors 选择重新参与。
失败时 stdout 不写入任何内容,stderr 恰好写一个对象:
{
"error": {
"category": "validation",
"message": "Ledger has 3 error(s). Pass --allow-errors to report anyway.",
"exit_code": 1,
"details": ["main.bean:1: Transaction does not balance: (2.50 USD)"]
}
}五个退出码和每个在 JSON 错误对象中携带的 category 字符串。脚本按数字分支;人类读取类别。
| 代码 | 类别 | 含义 |
|---|---|---|
| 0 | 无 | 成功,包括预览和故意的重复跳过 |
| 1 | validation | 账本或验证错误,以及任何其他运行时失败的兜底 |
| 2 | usage | 参数错误、缺少目标或多余,或在 --no-input 下需要输入 |
| 3 | auth | 认证或权限失败,包括只读目标 |
| 4 | conflict | 并发更改、需要重复审核的导入,或结果未知的写入 |
两个细节对任何在失败时重试的人都很重要。非零退出并不普遍意味着什么都没有改变:add transactions --partial 可以写入接受的记录,format -i 处理多个文件时可以在一个失败前重写一些,cloud ledger create --clone 可以在克隆失败前创建账本。在重试变更操作前读取 error.result。托管命令把服务器的 HTTP 状态映射到同一张表,保留服务器自己的消息:401 和 403 退出码为 3,400 为 2,409 为 4,其他所有情况(包括速率限制)退出码为 1。CLI 无法知道结果的写入,如删除中途超时,退出码为 4 并说明,而不是猜测。
自动化指南端到端地演示了一个 jq 管道处理这个封装。
随行的修复
对齐发布也是关闭首个发布暴露的缺陷的机会。这些修复落在两个标签之间,每个都有回归测试:
- 数字以定点文本写入,绝不使用科学计数法,包括
bea init渲染的期初余额。写着1E+3的账本技术上是有效的,但实际不可读。 - 成本批次在 JSON 序列化中保留日期和标签,批次标签在写入交易时正确转义。
- 导入期间显式零分录是真实金额,而不是被读为"省略,请帮我平衡"。
- CSV 导入通过一个严格的读取器。 表头发现过去会剥离列名,而提取保留原始键,因此文档承诺接受的填充表头会作为缺失列失败。现在名称只剥离一次,映射的列必须恰好出现一次,未闭合的引号在写入任何内容之前以其行号失败。
- BQL 加载准确的账本路径,而不是 URL 解析的连接字符串,因此不寻常的路径像 CLI 其余部分一样解析。
bea balance <term>只合计它显示的内容。 保留的父级不再报告被排除子级的合计,无关的未定价持仓不再使 USD 选择失败,封装报告应用的过滤器。报表上格式错误的--account模式以退出码 2 退出,作为它应得的使用错误。- JSON 模式 stderr 始终是一个对象,即使在失败前有被容忍的警告。
- 托管凭据及早且一致地失败:包含空白的
BEA_TOKEN在任何请求前被拒绝,撤销的凭据由cloud status和账本命令以相同方式报告,owner/name在确认提示或认证调用前被验证。cloud logout保留BEA_TOKEN,cloud ledger list --json回显它实际服务的页面。 - Homebrew 配方固定了确切的 PyPI 工件 URL,因此 tap 安装和 PyPI 安装可证明是相同的字节。
在你看到之前如何验证
发布是一个声明,管道是证据。cli-v0.2.0 标签必须指向 main 上的一个 pyproject.toml 版本完全匹配的提交;工作流拒绝任何其他情况,包括预发布后缀。从那里开始:
- 完整检查套件首先运行。
make check-all覆盖 lint、格式化、严格 mypy、死代码检测、生成参考漂移检查和测试套件。发布拉取请求记录了 635 个测试通过。 - 引擎锁导出并哈希固定,源码分发和 wheel 构建一次。每一步后续测试那些确切的工件,而不是重建。
- 三个操作系统和两个 Python 上的干净安装。 wheel 通过
uv tool安装,sdist 通过pip在 Linux、macOS 和 Windows 上安装,Python 3.12 和 3.14,包括可选的 AI 额外功能。一个 Homebrew 任务在 macOS 和 Linux 上通过临时 tap 安装 sdist。 - 发布是顺序的且无令牌的。 PyPI 通过可信发布接收工件,因此没有长期存在的 API 令牌会泄露;GitHub Release 创建时带有发布证明;
Formula/bea.rb被推送到公共 tap,包含 PyPI 实际服务的 sdist URL 和哈希。 - 发布后冒烟测试从真实索引安装。 单独的任务从 PyPI 和公共 tap 安装固定版本,并针对已安装的可执行文件运行相同的客户冒烟测试。失败不回滚任何内容,但确实意味着在告诉任何人之前发布需要关注。
这篇文章写在第五步的另一侧。
从 0.1.0 升级
通过安装你副本的包管理器运行升级,或让 bea 来做:
$ bea upgrade --check # 报告已安装和最新版本以及将运行的命令
$ bea upgrade # brew upgrade bea,uv tool upgrade beancount-io,或 pipx upgrade beancount-io包管理器完成后,bea upgrade 刷新托管引擎,使两者保持配对。然后检查三件事:
- 任何运行
bea format PATH重写文件的脚本现在需要bea format -i PATH。旧默认值无法预览,新默认值可以。 - 任何依赖
format捕获语法错误的脚本应该为那调用bea check,因为格式化不再解析。 - PyPI 安装在上次升级后的第一个本地命令需要一次网络和
uv,以便配置引擎。Homebrew 安装不需要任何东西。
脚本已经解析的所有内容、封装键、十进制字符串和退出码都不变。封装中的 bea 字段现在读作 0.2.0。
此版本不做的事
- 托管目标未实现。 没有
--ledger标志;本地命令读取本地文件,绝不隐式上传。托管账本在bea cloud下管理,作为 git 克隆进行工作。 bea ask仍然需要ask额外功能和 Beancount.io 凭据,并且不支持--json。默认安装不携带 AI 依赖。- Beangulp 和 Beanprice 是可选加入,Beangulp 需要系统的
libmagic库。bea import --csv在没有这两者的情况下覆盖银行导出。 - 转发的原生命令不发出封装。 如果你需要 doctor 操作的结构化输出,这是我们想听到的请求。
自标签以来,main 已经拾取了 0.2.0 的第一轮 QA,它将搭载在下一次发布中:bea format 读取 stdin 作为过滤器,其 -o FILE 模式以命名写入内容的封装应答;--json check 拒绝仅 bean-check 的标志,--json 在 doctor、example 和 treeify 上被直接拒绝,因此脚本不能把原生文本误认为封装;--json query -o FILE 原子地写入封装到文件,--numberify 也应用于 JSON;bea engine status 命名哪个引擎层级在服务;以注释开头的 BQL 查询运行;原生透传 --help 在引擎配置之前工作;查询 shell 的 .output 在重定向失败后恢复原始流。
下一步去哪里
- CLI 快速入门:安装、第一个账本、第一笔购买、第一次余额检查。
- 你和 bea 的第一个月:从
init到对账的月末报告。 - 导入银行导出:无 Python 的 CSV 路径、规则文件和 Python 导入器。
- 用 bea 自动化记账:解析账本、读取封装、按退出码分支、调度。
- Beancount CLI 参考:每个命令、选项、环境变量和退出码,对照 CLI 的生成参考检查。
- 给你的 AI 代理一个账本:0.1.0 发布中的代理优先演练。
- 变更日志:每个发布,最新的在前。
让账本保持代码化
一个可以一行安装的工具链是可以交给任何人的工具链:联合创始人、簿记员、CI 运行器、AI 代理。Beancount.io 提供保持透明、版本控制和可复现的纯文本记账,bea 是保持本地账本诚实的命令,托管服务是你的团队、手机和助手相遇同一账本的地方。安装 bea 并运行你的第一次检查,如果发布做了什么出乎意料的事,GitHub 仓库是我们想听到的地方。





