接棒 Maybe Finance 狂揽 1.5 万 Star:拆解开源个人财富引擎 Sure 的双向账本、49 个 MCP 工具与 Pipelock 零信任 AI 架构

接棒 Maybe Finance 狂揽 1.5 万 Star:拆解开源个人财富引擎 Sure 的双向账本、49 个 MCP 工具与 Pipelock 零信任 AI 架构

当一家烧掉近 100 万美元研发经费的明星开源个人财务项目停摆归档后,接盘它的社区分支还能走多远?最近我拉取并通读了 Sure(GitHub 星标已突破 1.53 万)的主干源码。它继承了原 Maybe Finance v0.6.0 的 AGPLv3 代码遗产,却在社区手中进化成了一套极其硬核的现代个人财富与数据主权中枢:底层是 Rails 8.1 驱动的双轨账本与 28 家银行券商连接器,上层则率先打通了包含 49 个原生工具的 MCP 协议栈、Pipelock 3.8.0 零信任 AI 网关,甚至还配套了 Tauri 2 桌面端与 Apple FinanceKit 移动端回传通道。

不同于常见的“记账皮套 + 调个 LLM 聊天”玩具项目,Sure 的代码库里藏着大量解决真实生产级金融边界痛点的工程细节。从防币种隐式混算的 Money 防火墙、手动与直连双轨并行的日终余额物化引擎,到“只存 SHA-256 与引文、绝不经 MCP 泄露原件字节”的溯源式财富代理设计,我将结合核心源码逐层拆解这套系统的架构哲学。

[!NOTE] 源码审查基线说明
本文所有目录结构、模型行为、协议细节与工具清单,均基于我对 we-promise/sure 主干仓库源码(覆盖 app/models/、app/controllers/mcp_controller.rb、docs/llm-guides/、desktop/、mobile/ 与 compose.example.ai.yml 等核心模块)的逐行审计与结构梳理。


一、系统全景:一个 Rails 8.1 内核驱动的多端财富中枢

Sure 的核心定位非常克制且清晰:所有客户端都围绕同一台由用户自托管或受控的 Rails 8.1 服务端运转。在 db/schema.rb 中可以看到,系统一共维护了 159 张数据表,以 PostgreSQL(支持可选 pgvector 本地向量检索)和 Redis + Sidekiq 作为持久化与异步队列底座,外部则向五类入口统一输出同一份家庭账本真相:

客户端形态 核心技术栈 定位与信任边界 关键源码路径
Web 主端 Rails 8.1 + Hotwire (Turbo/Stimulus) + ViewComponent + Tailwind 完整功能与管理中枢,使用设计令牌 sure-design-system.css 严禁裸色值 app/controllers/、app/components/DS/
macOS 桌面端 Tauri 2 + WKWebView(Universal 通用二进制) 原生窗口外壳,启动先探活 {server}/up 再加载标准登录与 SSO/MFA desktop/src-tauri/src/
iOS / Android 端 Flutter + Material 3 + Apple FinanceKit OAuth 2.0 登录、资产查看,iOS 端充当 Apple Wallet 交易发布器 mobile/lib/、docs/providers/financekit.md
外部 AI Agent Streamable HTTP JSON-RPC 2.0 (POST /mcp) 对外暴露 49 个财务读写工具,支持 OAuth PKCE 动态注册与静态 Bearer 令牌 app/controllers/mcp_controller.rb
开放 API 客户端 Doorkeeper OAuth2 / X-Api-Key + OpenAPI (rswag) 供自定义脚本与三方集成调用的 /api/v1 端点,含现金流 Sankey 图计算 app/controllers/api/v1/、docs/api/openapi.yaml

我在阅读仓库根目录的 AGENTS.md 时注意到一个很有意思的工程约束:项目明确反对在 Rails 中堆砌 app/services/ 大杂烩,而是推崇以 Rails Concern 和轻量 PORO(Plain Old Ruby Objects)围绕领域模型组织代码(例如调用 account.balance_series 而非 AccountSeries.new(account).call)。同时,全家(Family)作为最高租户边界,所有请求必须严格经由 Current.user 与 Current.family 以及单账户共享作用域过滤,连内置的 MCP 工具也不例外。


二、核心账本物理学:9 类代理账户与双向余额物化引擎

个人财富管理软件最容易写出“玩具感”的地方,在于只把银行流水当成一张简单的流水表格来累加。一旦同时存在手动维护的房产/现金账户、银行 API 直连但可能缺失早期流水的支票账户、以及持仓随市价每日波动的证券账户,单纯正向相加或单纯抓当前余额都会迅速算糊涂。

Sure 在 app/models/concerns/accountable.rb 与 app/models/entryable.rb 中使用了 Rails 原生的 Delegated Types(委托类型) 来解耦这两条轴线:

  1. 9 类 Accountable 账户载体:资产端划分为 Depository(现金储蓄)、Investment(投资账户)、Crypto(加密货币)、Property(房产)、Vehicle(车辆)、OtherAsset(其他资产);负债端划分为 CreditCard(信用卡)、Loan(贷款)、OtherLiability(其他负债)。
  2. 3 类 Entryable 账目分录:Transaction(改变现金流向的收支交易,可挂载分类、商户、标签与附件)、Trade(证券买卖数量与单价变动)、Valuation(某个日期的绝对账户估值或负债快照,不计入日常收支)。
  3. 符号约定铁律:在现金流向(Entry#amount)上,负数代表资金流入(Inflow),正数代表资金流出(Outflow)。例如支票账户存入一笔钱是负分录,信用卡还款减少负债也是负分录;而 Valuation 则直接代表绝对市值,绝不套用现金流正负号。

更核心的是 app/models/balance/materializer.rb 背后的双轨日终余额物化机制。我在对比了 Balance::ForwardCalculator 与 Balance::ReverseCalculator 后,画出了下面这张双轨计算拓扑图:

Sure 双向余额物化引擎与多币种防线架构

2.1 手动前向路径 vs. 银行直连反向路径

  • 轨道 A:手动账户走前向增量计算(Balance::ForwardCalculator)
    手动账户以 opening_anchor_balance 为起点,顺着时间轴 calc_start_date.upto(calc_end_date) 向前推进。为了避免每次改一笔历史账目就重算几年的余额,我在 Entry#sync_account_later 中看到它巧妙利用 @earliest_saved_date 与 date_previously_was 计算受影响的最早日期作为 window_start_date,仅从 prior_balance(前一天已落库余额)起算。
    不过,ForwardCalculator#resolve_starting_balances 也极其严谨地留了两个自动回退全量重算的保险丝:一旦发现该账户存在非现金持仓(end_non_cash_balance != 0,持仓市值需由 Holding::Materializer 全量更新)或属于跨币种账户(multi_currency_account?,可能依赖事后才补齐的历史汇率),立刻放弃增量种子,回退到开户锚点重新推演。

  • 轨道 B:直连账户走反向锚点推演(Balance::ReverseCalculator)
    外部银行 API(如 Plaid、SimpleFIN、Enable Banking、IBKR、Wise 等 28 家内置连接器 *_item.rb)往往只能拉取最近 90 天或几个月的交易流水,却能给出最新的实时余额。因此 Sure 反其道而行之:以银行上报的 current_anchor_balance 为基准,沿着 current_anchor_date.downto(calculation_start_date) 从今天倒推历史!
    如果中途存在多次官方对齐校准点(valuation.entryable.reconciliation?),计算器会在该日期硬重置日终余额,彻底消除“缺了几笔早期流水导致整条历史净值曲线整体偏移”的漂移问题。此外,Account::Syncer#report_anchor_dated_away_from_holdings 还会专门巡检券商持仓日期与余额锚点日期的时间差(解决 IBKR 与 Plaid 常见的 T+1 错位导致当日盈亏被误算为现金流的经典坑),并自动写入 DebugLogEntry 告警。

2.2 生产级账本的三大防线

在细节处理上,Sure 展现出远超一般开源记账工具的防错密度:

  1. 多币种隐式混算防火墙(lib/money.rb):
    Money 类显式禁止对不同币种执行加减或大小比较。只要未显式调用 exchange_to(currency, date:) 转换到指定日期的汇率,开发与测试环境直接抛出 Money::CurrencyMismatchError 阻断;生产环境则上报错误追踪(每进程上限 REPORTED_MISMATCHES_LIMIT = 500)并维持原展示不崩溃,防止脏数据污染净资产汇总。
  2. 同步保护锁与 Revolut 等 ASPSP 的 Pending 穿透(Account::ProviderImportAdapter):
    一旦用户在界面上修改了某笔交易的分类、商户或备注,分录会被标记 user_modified = true 并在 locked_attributes 记录字段级锁时间戳,后续银行同步再也不会覆盖用户的手工整理。但我在 provider_import_adapter.rb 第 64~108 行发现了一个精妙的例外通道:像 Revolut Italy 这类银行,在交易从“待入账(Pending)”转为“已入账(Booked)”时会复用同一个 external_id。如果用户趁它还在 Pending 时就提前分了类,常规保护锁会导致那枚 Pending 徽章永远无法消退。因此 Sure 在确认拦截保护锁的同时,专门开了一个“Pending → Booked 状态清理旁路”以及“仅刷新供应商自有 extra 命名空间(绝不覆盖用户手动输入的 exchange_rate)”的精细机制。
  3. 跨账户转账与现金流归属区分(Family::AutoTransferMatchable & Transaction):
    同步完成后,系统在默认 4 天时间窗口与 10% 跨币种汇率容差(UI 未提示时的轻量线索收敛到 2 天 / 3%;对同一个 WiseItem 下的多币种账户则刻意跳过猜测,因为 Wise 内部兑换必须走官方 Activity 接口配对)内自动匹配跨账户转账。更难得的是,它在 Transaction::BUDGET_EXCLUDED_KINDS 中做了极其清醒的会计区分:普通转账(funds_movement)和信用卡还款(cc_payment)不计入支出,但还贷(loan_payment)与投资定投(investment_contribution)依然保留在现金流支出统计中;同时 Entry#split_once! 通过 with_split_lock 按固定顺序对 transaction 与 entry 加 FOR UPDATE 行级锁,彻底杜绝并发重复拆单。

三、49 个 MCP 工具、溯源优先架构与 Pipelock 零信任防线

如果说双轨账本是 Sure 的下盘,那么它对 AI Agent 生态的接入方式则是我见过最清醒、最克制的架构实践之一。

很多项目一提“AI 记账”,做法就是把全部数据库表丢给 RAG 或者放开 SQL 读写让大模型自由发挥。Sure 在 docs/llm-guides/wealth-agent-harness.md 与 app/controllers/mcp_controller.rb 中提出了一套截然不同的 “溯源优先(Provenance-First)”双半边架构:

Sure 溯源优先 MCP 财富代理架构与 Pipelock 零信任安全泳道

3.1 为什么 Sure 拒绝把“原始文档字节”交给 MCP?

在 Assistant.function_classes 中,Sure 为内置聊天助手和外部 POST /mcp 端点共用了同一套工具注册表——包含 35 个基础工具与 14 个预览期工具(共 49 个工具),涵盖账户查询、收支与 Sankey 现金流分析、自动规则编排、账单与发薪周期计划(get_paycheck_plan)、以及对账单金库(Statement Vault)。

外部客户端(Claude Desktop、Cursor、VS Code 或 OpenClaw)既可以通过符合 RFC 规范的 OAuth 2.0 动态客户端注册(POST /register + PKCE,/.well-known/oauth-protected-resource) 拿取授权令牌,也可以在自托管环境配置 MCP_API_TOKEN + MCP_USER_EMAIL。且 McpController 为每次请求建立瞬态会话(Transient Session),调用完立刻销毁,绝不跨请求泄漏用户身份或管理员模拟状态。

但在“对账单金库(Statement Vault)”的设计上,Sure 立下了一条反直觉却极其安全的铁律:upload_account_statement 接收最大 25 MB 的 PDF/CSV/XLSX 并按 SHA-256 幂等去重存档,但 MCP 接口永远不向任何 Agent 回传文件原始字节,也不把这些对账单塞进向量库!

[!IMPORTANT] Parse First, Publish Second(本地解析优先,再向 Sure 发布)
Sure 的设计者认为,PostgreSQL 数据库行是可变的,无法替代 Git 仓库的不可变审计快照与黄金测试(Golden Tests)。因此,外部 Agent Harness(如本地运行的 Claude Code / OpenClaw 工作区)应当自己保管可解析的原件主本、运行确定性的 Python 提取器与报税逻辑;然后以 SHA-256 作为跨系统联合主键 调用 upload_account_statement 归档,借助 get_statement_coverage 检查每个月度的 covered / missing / mismatched / ambiguous 缺口地图,最后把结构化资产数据发布回 Sure。

3.2 防止 AI 幻觉写坏账本的双重硬门禁

为了防止外部 Agent 在财务账本上“自由发挥”,我在 app/models/assistant/function/ 里看到了两套硬核的契约护栏:

  1. 估值引文强制文法(record_valuation.rb):
    对于房产、私募股权、收藏品等无法自动拉取报价的资产,Agent 调用 record_valuation 时必须传入严格符合以下文法的 source 字段:
    ["estimated: "] citation [" (grade: A|B|C)"]
    
    其中 grade: A 代表来自当日官方正式文件,B 代表基于文件推导,C 代表代理估算且 estimated: 前缀必须附带评级。哪怕少写一个小写字母、写了无效的 grade: D,或者把 estimated: 首字母大写,服务端都会直接拒收报错——“没有出处的数字比缺失数字更危险,因为它看起来像权威事实”。
  2. 规则激活令牌锁(apply_rule.rb):
    外部 Agent 调用 create_rule 或 update_rule 时,规则强制以未激活(inactive)状态保存;想要调用 apply_rule 批量回溯历史交易,必须携带 30 分钟内通过 preview_rule 签发的 preview_token。如果规则条件在预览后被改动、或匹配集合发生了漂移,或者该规则包含了会消耗 Token 费用的 AI 分类动作,接口会直接拒绝执行。

3.3 把 AI 流量关进 Pipelock 3.8.0 零信任反代

在自托管部署模板 compose.example.ai.yml 与 pipelock.example.yaml 中,Sure 官方直接默认编排了一层独立的 Pipelock 3.8.0 安全代理容器:

  • 入站 MCP 审查(端口 8889):开启 mcp_input_scanning: action: block 与 on_parse_error: block 强阻断 Prompt 注入;开启 mcp_tool_scanning: detect_drift: true 检测工具投毒与元数据漂移;用 tool_chain_detection 在 20 次调用滑动窗口内监控危险工具组合链路;
  • 出站 DLP 防泄露(端口 8888):Web 与 Sidekiq Worker 的外发模型请求通过 HTTPS_PROXY 走 Pipelock 隧道,扫描环境变量密钥与敏感头泄漏;
  • 防篡改黑匣子(flight_recorder):支持挂载密钥文件为每笔 MCP 决策生成带哈希链签名的审计回执。

四、AI Plumbing 2a 拆解:为什么在 RubyLLM 之外自建薄传输层?

在 2026 年 11 月刚刚合入主干的 AI plumbing 2a 重构(app/models/ai/)中,Sure 交出了一份非常值得后端工程师学习的 LLM 基础设施抽象样本。

很多人接入了像 ruby_llm 这样的多模型 SDK 后,习惯直接在业务代码里调 SDK 方法、把超时和重试全部托管给第三方库。Sure 在 docs/llm-guides/ai-pipeline.md 与 app/models/ai/transport/base.rb 中明确指出这样做的三大隐患,并搭起了一套四层垂直堆栈:

Sure v0.8 AI 传输层与可观测性四层垂直堆栈
  1. 接管重试主权与总时间预算(Ai::Transport::Base#attempt):
    Sure 强制将所有 RubyLLM 上下文的 max_retries 设为 0。原因很现实:SDK 内部重试之前没有钩子函数,无法在重试间隙检查账户余额是否已经耗尽(比如 OpenAI 的 429 insufficient_quota),也无法防止多轮工具调用把超时拖垮。在 Ollama 等本地模型场景下,因为模型流式关闭、单次请求超时 OPENAI_REQUEST_TIMEOUT=300s,compose.example.ai.yml 专门限制工具最大轮次为 2 轮,并由 Ai::CallContext 计算全局余量 within_budget——甚至在响应返回之后还要再调用一次 check_budget!(after: true),防止迟到的结果被误判为有效。
  2. 拦截本地模型张冠李戴(check_answering_model!):
    本地跑过 LM Studio 的开发者都遇到过一个隐蔽坑:你请求模型 A,如果它没加载,LM Studio 不会报错,而是直接用当前内存里加载的模型 B 回复一个 HTTP 200。Ai::Transport::Base 会自动剥离厂商前缀(如 openai/)和日期快照后缀(-2025-04-14)做比对,一旦发现应答模型货不对板,立刻抛出 Ai::Error::ModelMissing。
  3. 单一网络出口契约测试(Ai::Transport::Http):
    由于 RubyLLM 自身不直接暴露 TLS 证书与代理细粒度参数,Sure 手写了 Http.ruby_llm_adapter 作为唯一的 Net::HTTP 适配器注入给 RubyLLM;并在测试套件 test/architecture/ai_transport_contract_test.rb 里用契约测试死死看住 app/models/ai/ 目录——任何人在传输层之外私自 OpenAI::Client.new、Anthropic::Client.new 或 Faraday.new 都会直接让 CI 爆红。
  4. 密钥闭包脱敏与单次计费入账:
    每次调用通过 ActiveSupport::Notifications 广播 *.ai 与 usage.ruby_llm 事件,UsageSubscriber 统一写入 llm_usages 表;传入事件负载的不是明文 API Key,而是一个 Redactor.scrub 脱敏闭包函数,确保异常日志与 Langfuse 追踪中绝不出现凭据明文。

五、全端形态与自托管安全性:Tauri 2、FinanceKit 与加密密钥演化

除了后端核心,Sure 在客户端与自托管安全细节上也有几个相当亮眼的设计:

  • Tauri 2 桌面端与 Flutter + FinanceKit 移动端:
    macOS 桌面端并没有用 Electron 打包浏览器,而是用轻量的 Rust Tauri 2 驱动系统原生 WKWebView(desktop/src-tauri/src/),处理好单点登录回调(sso.rs)、系统通知和角标。而在移动端,除了 Flutter 跨平台客户端,Sure 还为 iOS 打造了 Apple FinanceKit 设备发布器(Device Publisher):服务端本身无需安装任何 Apple Wallet 证书或私钥,由 iOS App 经用户 StoreKit 与系统授权后直接从本机钱包读取记录,写入本地有序发件箱(Outbox),通过一枚只写不可读、可随时吊销的专用上传凭据批量推送给 Sure 的 FinancekitInboxJob 队列;如果用户解除绑定并选择 discard,后端还会启动异步清理任务抹除该谱系来源的交易并重算原账户的历史余额。
  • ActiveRecord 加密与零配置自托管陷阱防线(lib/active_record_encryption_config.rb):
    个人财务库里存着大量银行访问令牌与 MFA 密钥。Sure 允许自托管用户在未显式设置 ACTIVE_RECORD_ENCRYPTION_* 三元组时,从 SECRET_KEY_BASE 确定性派生加密密钥;但为了防止新手直接拿 compose.example.yml 里公开过的示例秘钥上线生产环境,源码里直接硬编码了一份 KNOWN_COMPROMISED_SECRET_KEY_BASES 黑名单用于健康巡检告警!同时,模型层引入了 CURRENT_BACKFILL_VERSION = 10 清单,配合 CI 测试强制校验:未来任何人给任意新接入的金融连接器模型加上 encrypts 声明而忘了同步写入 BACKFILL_MANIFEST,测试套件会直接拦截合并。

六、实际部署边界与踩坑提示

在决定把 Sure 部署进自己的 Homelab 或二次开发之前,结合我通读 compose.example.ai.yml、docs/clients.md 与架构指南的观察,有三个客观边界值得提前心中有数:

  1. Docker 容器内 Yahoo Finance 同步的 IPv6 挂起坑:在没有配置好 IPv6 出口的 Docker 宿主机上,fc.yahoo.com 和 query1.finance.yahoo.com 往往优先解析出 IPv6 地址导致行情同步连接超时卡死,部署时需要显式配置 IPv4 DNS 或在 extra_hosts 固化 A 记录。
  2. 本地模型非流式响应与工具链超时计算:若接 Ollama 跑本地工具模型(如 llama3.1:8b),Sure 的外部对话在本地模式下是非流式返回的,且工具调用轮次直接放大响应耗时,必须严格按照 (1 + ASSISTANT_MAX_TOOL_CALL_ITERATIONS) * OPENAI_REQUEST_TIMEOUT + 余量 来放大 AI_RESPONSE_TIMEOUT,否则多轮查账必触发超时中断。
  3. macOS 未公证桌面端首次启动限制:官方 Release 提供的通用 .dmg 目前采用 ad-hoc 签名而未经过 Apple 公证(Notarization),在 macOS Sequoia 上首次拖入应用程序目录后需要到“系统设置 → 隐私与安全性”点击“仍要打开”,或在终端执行 xattr -cr /Applications/Sure.app 并允许局域网设备发现权限才能连上内网自托管服务端。

七、总结

从商业受挫归档的 Maybe Finance 废墟中接过火炬,we-promise/sure 并没有停留在“改个 Logo 继续修 Bug”的浅水区,而是用令人印象深刻的工程克制和边界感,回答了一个关键问题:在 AI Agent 时代,个人财务软件到底应该扮演什么角色?

它的答案非常笃定——做绝对严谨的“财务事实真源(System of Record)”:底层用正反双轨物化引擎、严格币种防火墙和并发锁保证每一分钱都经得起对账;中间用 49 个受契约约束的 MCP 工具与 Pipelock 零信任代理把读写权限关进笼子;外部则鼓励 Agent 在自己的沙盒里自由计算和推理,只把带着 SHA-256 哈希指纹与分级引文的确定性结果写回账本。无论你是想自托管一套真正不丢账的家庭资产总账,还是打算用 Rails 8 构建带 MCP 安全网关的个人 AI 基础设施,这个仓库的源码都非常值得一读。


原文链接与参考资料