回首页

abei —— 会自己读账单的记账工具

  • TypeScript
  • Node.js
  • Hono
  • Drizzle
  • PostgreSQL
  • pg-boss
  • zod
  • React

github.com/xingqiux/abei

我自己的记账系统,MIT 许可开源。账单从邮箱进来,读成流水,和已记的账对上,AI 预填,我只确认。网页给人用,CLI 和 API 把同一套能力交给 AI Agent。一句话的目标:每月花半天以内,每个账户的余额和银行 App 对得上。

abei 本机运行界面:交易列表,内容为演示数据。
本机运行截图,演示数据。

为什么做

国内的账单分散在支付平台和几家银行。同一笔钱常常出现两次,银行一次、支付平台一次;提现和退款的方向容易记反;余额对不上时,只能直接查数据库找原因。

这些平台都不开放接口,只能靠邮件。只有一个来源每天自动发账单,另外四个每月要去 App 里申请,前后大约 20 步、4 个解压密码。

从一开始就定下一条:发给模型的内容里,不能有卡号、订单号、密码或验证码。

主循环

邮件或上传,经过读取器,变成统一格式的流水行,再经过渠道语义、匹配、提案、收件箱确认、入账,最后由阿贝回答关于账的问题。每一环只有一个入口、一个落库的状态、一个数。

代码是 pnpm monorepo,六个包:contracts、db、server、cli、web、admin。一个服务进程同时跑 API、任务队列和内置 AI 阿贝。

邮箱IMAP 收取解析成统一结构LLM建议分类人工确认入账 邮箱 IMAP 收取解析成 统一结构LLM 建议分类人工 确认入账

几个关键决定

读取器只报事实,含义交给一张表

旧的解析器自己决定正负号,某个支付平台「不计收支」的行被记成了收入。现在读取器只报账单上写了什么:金额,收、支还是不计,以及原始的类型和状态。含义(转账、退款、不计收支)交给一张渠道语义表,规则一律是锚定的正则,按优先级取第一条命中;每个渠道最后都有一条按符号兜底并交给人看的规则,新出现的类型不会被瞎猜。在全部真实数据上,没有一行落空,还修正了 24 行被记成收入的错。规则可以在后台编辑,保存前能预览哪些行会变。

一种账单一个读取器,用样本回放

以前改一个解析要过五层配置。现在每种账单是一个代码读取器,输出同一种行结构,每个都带脱敏样本和期望结果:改了就回放,一行都不能变。带密码的 zip、GBK 编码探测、CSV、XLSX、PDF、邮件这些通用处理集中在一处。拿 49 份文档、2124 行真实数据只读回放,和旧流程逐行一致;其中一家银行的 PDF 改成按列位置读取后,修好了旧版约 27% 的行对方户名错位。

一个数,落在库里

「需要人处理」以前在四个地方各算一遍,同一页上会同时出现 20 和 25。现在行状态落库:8 种状态,每次跃迁写一条事件(谁、何时、依据、能否撤销)。旧的状态列改成只读的生成列,写不进去;约束规定已入账的行必须挂交易,被忽略的行必须写理由。

一份能力目录,派生四端

CLI 会推荐不存在的命令,试运行能过、真写却失败,Agent 还有五处绕开 API。现在每条能力只用 zod 定义一次,HTTP 路由、CLI 命令、阿贝的工具和 OpenAPI 都从它生成。试运行在真实事务里跑同一个函数,最后回滚,所以「试运行过了、真写失败」在结构上不会发生。共 70 条能力:读 27 条、写 33 条,另有 10 条只能由人确认。

AI 碰钱的边界

  • 外部 Agent 通过 CLI 接入,不走 MCP,因为命令行对模型来说更省 token。没有直接发 HTTP 的命令,模型只能看到建过模的能力。
  • 需要确认的能力,模型和 Agent 令牌都拿不到。在 CLI 上要加 --confirmed-by-human,AI 不许自己加。
  • 发给模型之前先脱敏:去掉黑名单字段(账号、交易号、订单号、原始列、密钥),所有连续 4 位以上的数字打星。已知的缺口也记下了:这只覆盖网页助手,终端那条路拿到的还是原始数据。
  • 我故意用较弱的模型来试 CLI,逼着帮助文字和报错写得足够清楚。

重建

第一版是 Rust 后端加 Firefly III 的 PHP 分叉:Rust 3.3 万行,PHP 13.7 万行。单用户、以 I/O 为主的应用用不上 Rust 的长处,代价却天天在付:没有 ORM,一张表长到 43 列;AI SDK 都在 TypeScript 那边;Agent 写 Rust 又慢。

42 个问题里没有一个是 Rust 造成的,但 Rust 让修每一个都更贵。

我在旧代码旁边重建:Node 22、Hono、Drizzle、pg-boss、zod、Vitest。大约四天后,读取器、语义、匹配和回放基线都跑起来了;随后删掉了 127 个 Rust 文件和 1506 个 Firefly 文件。

  • 集成测试只连名字以 abei_test 开头的库,迁移命令拒绝指向开发库。
  • 回放拿三个数和一份 886 行人工裁决的基线比对(自动处理 75.7%,打断 199 次,漏错 1.8%),有任何偏离就失败。
  • 28 条架构决策记录,每条写明决定了什么、推荐、代价和能否撤回。

学到的

  • 测试全绿不等于能用。端到端验收时修的 10 个问题里,有 4 个是单测全绿、主链路却走不通的阻塞。一次脚本试走记了 33 步「走通」,对照截图人工核对,真走通的是 21 步。
  • 它是按平台的规格造的,实际是一个人每月用一次:43 张表、70 条能力,够用的版本大约是 27 张表、50 条。下一步是用,而不是再加:用新版把九月的账整理完,每个余额都对上。

时间线

  1. 2026.07最早的 Rust 后端封存
  2. 2026.08.07基于 Firefly 的版本:React 前端、命令行和 Agent
  3. 2026.08.31真实整理一个月,记下 43 个问题
  4. 2026.09.01新架构定稿:自建单式账本
  5. 2026.09.03在旧代码旁边用 TypeScript 重建后端
  6. 2026.09.04五个读取器完成;2124 行真实回放一致
  7. 2026.09.17开源