mcpField
B
Beancount.io

Beancount.io

远程服务
github.com金融数据318今天更新

关于 Beancount.io

Beancount.io Backend 是 Beancount.io 的 API 与后台任务服务,使用 Node.js 和 TypeScript 构建。文档涵盖账本 API、身份验证、计费、OAuth 部署及可选的 MCP Registry 域名证明;并明确指出令牌检查不是 MCP 工具。

主要功能

  1. 01GraphQL 和 REST API
  2. 02PostgreSQL 数据存储
  3. 03Redis 缓存
  4. 04后台数据处理任务
  5. 05身份验证与 OAuth 支持

适用场景

  1. 01集成账本 API
  2. 02运维后端部署
  3. 03配置 OAuth 与凭证检查

MCP 相关

后端可提供用于验证 Beancount.io 向官方 MCP Registry 发布内容的域名证明。文档明确说明令牌检查操作不是 MCP 工具。

配置

远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。

服务地址https://beancount.io/api-gateway/mcp
一键复制安装命令
Claude CodeTerminal
Cursor.cursor/mcp.json
VS Code.vscode/mcp.json
Claude Desktopclaude_desktop_config.json
Windsurf~/.codeium/windsurf/mcp_config.json

命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。

工具

详细介绍

Beancount.io 后端

概述

后端使用 Node.js 和 TypeScript 构建,采用 Koa 和 GraphQL,为 Beancount.io 提供 API 和后台作业。主要功能包括:

  • 通过 Apollo Server 和 Koa 提供 GraphQL API
  • 使用 TypeScript 构建类型安全的代码库
  • 使用 PostgreSQL 和 Drizzle ORM 执行数据库操作
  • 使用 Redis 进行缓存
  • 用于数据处理的后台作业

有关密码准入、API 配额、电子邮件恢复和部署要求, 请参阅身份验证和速率限制。 注册时明确回应账户是否存在,是经过审慎考虑的产品 选择。频繁的 Git 自动化应使用 SSH 密钥,而不是反复通过 HTTP 提交账户密码。

账单

受支持的应用客户端为 beancount-web-prod,由 STRIPE_PRIVATE_KEY、STRIPE_PUBLIC_KEY 和 STRIPE_WEBHOOK_SECRET 配置。 每个账单门户会话都使用固定在 SUBSCRIPTION_CONFIG 中的配置,该配置仅允许管理付款方式、发票和账单 详情;套餐变更通过 changePlan 进行。账单 使用实时订阅以及可识别的当前/旧版价格;旧的缓存周期 或未知价格无法确定已购买的套餐。只有经过验证的实时 Webhook 才能刷新客户缓存。Stripe 失败不会清除已存储的周期。

GA4 purchase 事件由仪表板在浏览器中于付款确认后记录;后端不发送分析数据。付费 Checkout 会返回到仪表板确认页面(/settings/billing?checkout=success&session_id=…), 而 changePlan 会报告即时变更所收取的发票信息 (invoiceId、amountPaid、currency),以便仪表板只计入一次。

已停用的 beancount-web-dev 租户和 STRIPE_DEV_* 变量会被忽略; 没有虚假的付费脚本取代它们。请将 Stripe 隔离测试放在实时应用账单流程之外。

项目结构

backend-v2/
├── docs/           # API and operator guides
├── scripts/        # Deployment migrations and developer tools
├── src/
│   ├── config/     # Environment-backed application configuration
│   ├── drizzle/    # PostgreSQL setup and tracked migrations
│   ├── features/   # Domain adapters, workflows, services and models
│   ├── foundation/ # Composition, external clients and Redis
│   ├── scheduler/  # Scheduled jobs, including authentication mail
│   ├── server/     # Koa, GraphQL, REST and MCP transport setup
│   └── shared/     # Errors, caching, logging and utilities
├── server.ts       # Application entry point
└── package.json

开始使用

本指南假设你使用类 Unix 环境。如果你使用 Windows,请参阅在 Windows 上运行。

前置条件
  • Node.js 20+
  • PostgreSQL
  • Redis
  • Git
设置
  1. 克隆仓库:
git clone [email protected]:bex-co/beancount-io.git
cd beancount-io/backend-cluster/backend-v2
  1. 安装依赖:
yarn install
  1. 设置环境变量:

在 backend 目录中创建 .env 文件,并填写必要的环境变量。必需的变量包括:

POSTGRES_BACKEND_URI=postgresql://user:password@localhost:5432/beancount
REDIS_URI=redis://localhost:6379
FAVA_API_URL=http://localhost:8000
FAVA_API_ADMIN_USER=admin
FAVA_API_ADMIN_PASSWORD=your_password
AUTH_SECRET=replace_with_a_strong_shared_deployment_secret
SERVER_URL=http://localhost:4104
DASHBOARD_URL=http://localhost:5173
# Optional: login/register JWT lifetime in minutes (default 525600 = 365 days)
# AUTH_JWT_EXP_MINUTES=5
# Optional: reverse proxies in front of this server that append to
# X-Forwarded-For (default 1; 0 when clients connect directly)
# TRUSTED_PROXY_HOPS=1

确保 AUTH_SECRET 非空,并在 API/worker 进程及重启后保持一致:它用于签署会话,并派生认证邮件加密密钥和恢复证明密钥。切勿提交其真实值。

  1. 启动服务前,应用数据库迁移:
node --env-file=.env scripts/migrate-deploy.js
yarn start

部署时,等效操作是运行 yarn migrate:deploy,并确保已注入环境变量。这包括 auth_mail 发件箱迁移;如果没有该迁移,API 启动后将无法将恢复邮件加入发送队列。常规服务启动时会自动启动其认证邮件 worker,每五秒运行一次。

开发/测试环境中的 ConsoleSendGrid 会抑制邮件发送,且绝不会打印邮件内容、地址、链接或 OTP。人工邮件/注册/恢复验证需要完整配置邮件部署:生产客户端使用 SEND_GRID_API_KEY 和已配置的发件人。自动化测试会捕获合成邮件,不会联系邮件服务提供商。有关 worker、Redis 和恢复所需条件,请参阅运维指南。

生产环境中的 OAuth 需要 OAUTH_JWKS,这是由部署密钥管理器提供的完整 P-256/ES256 私钥 JWKS。绝不能将其提交或复制到 .env.example 中。如果该变量缺失或格式错误,OAuth 端点会返回 503 oauth_not_configured;现有 API 和旧版 JWT 登录仍可用。开发和测试环境会生成仅在当前进程内有效的临时密钥。

轮换 OAuth 签名密钥时,先安装替换密钥并部署,然后从提供的 JWKS 中移除被替换密钥对应的公钥。如果只是从 Git 中删除历史私钥,却不替换当前接受的密钥,则不会使使用该密钥签署的令牌失效。

REST API 快速入门

个人访问令牌面向付费套餐用户开放。登录后,打开个人访问令牌,创建具有集成所需最低权限的令牌,并在显示时复制。之后无法找回明文;请将其保存在密钥管理器中,切勿提交。

从安全的账本列表端点开始:

export BEANCOUNT_API_KEY="bcio_your_token_here"

curl --fail-with-body \
  --header "x-api-key: $BEANCOUNT_API_KEY" \
  https://api.v3.beancount.io/api-gateway/v1/ledgers

交互式 API 参考展示了可用端点、请求架构和所需权限。

OAuth 部署约定

生产环境中的 DASHBOARD_URL 是公开的授权服务器和同意流程入口。SERVER_URL 是本地开发环境使用的直接后端 URL。它们是常规的公开部署配置,不是 OAuth 密钥。在生产环境中,两者默认都指向官方服务;在开发环境中,默认指向本地后端和仪表板。除生产环境外,仅 localhost、127.0.0.1 或 ::1 可使用 HTTP;其他情况都必须使用 HTTPS。如果生产反向代理通过路径前缀公开 Beancount,请将该前缀包含在 DASHBOARD_URL 中,且不要添加末尾斜杠。

本地主机堆栈使用不同端口,但主机名相同。生产部署通常应通过同一个 HTTPS 入口对外提供两者。

发现流程从根据 API 资源推导出的 RFC 9728 URL 开始。对于签发者 https://books.example.test/beancount,API 资源和发现链如下:

resource: https://books.example.test/beancount/v1
protected resource metadata:
  https://books.example.test/.well-known/oauth-protected-resource/beancount/v1
authorization server metadata:
  https://books.example.test/.well-known/oauth-authorization-server/beancount

代理必须将这些众所周知的路径、带签发者前缀的 /api-gateway/oauth/* 端点以及交互页面分别路由到此后端和仪表板。在仪表板主机(APP_DOMAIN)上,/.well-known/* 必须转发到此后端,以便 Apple 和 Google 能从与 /ledger/... 链接相同的源获取原生应用链接凭证。

从外部验证凭证

检查 Beancount.io 签发的凭证有两种方式,它们回答的问题不同。请慎重选择。

离线验证,使用已发布的密钥。 OAuth 访问令牌是包含 iss、aud 和 kid 的 ES256 JWT。其公钥发布在 /api-gateway/oauth/jwks,也可以通过上文的 RFC 8414 文档发现,因此任何标准验证器(jose 的 createRemoteJWKSet、API 网关的 JWT 过滤器)都可以在不调用我们的情况下验证令牌。

这可以证明令牌_确实由我们签发_且尚未过期。但它无法告诉你授权是否在五分钟前被撤销,也完全不适用于另外两种凭证:会话令牌使用 HS256(发布密钥就等于发布了签发此类令牌的能力),而 bcio_ API 密钥不是 JWT,没有可供检查的签名。

在线验证,向我们查询。 POST /api-gateway/v1/token/introspect(RFC 7662)可检查所有三种凭证,包括凭证_此刻是否有效_:

curl -sS https://beancount.io/api-gateway/v1/token/introspect \
  -H 'x-api-key: bcio_your_own_key' \
  -H 'content-type: application/json' \
  -d '{"token":"bcio_the_key_you_are_checking"}'
{
  "active": true,
  "sub": "usr_7wXzK9mN",
  "scope": "ledger.read ledger.write",
  "jti": "akey_7wXzK9mN",
  "iat": 1767225600,
  "exp": 1798761600,
  "bio_credential_kind": "apikey",
  "bio_assurance": "delegated",
  "bio_ledger_scope": "alice/main-ledger"
}

不可用的凭证会准确返回以下内容,不多也不少:

{ "active": false }

过期、格式错误、已撤销、从未签发以及属于其他用户的凭证都会故意返回完全相同的响应正文——如果响应能区分这些情况,调用方就可以枚举哪些凭证曾经有效,以及每个凭证何时被撤销。

在基于此功能构建之前,有三点需要了解:

  • 此接口绝不允许匿名访问。 此端点需要使用其自身的凭证。否则,它就会把任何被盗令牌变成“有效,用户 X,可写入账本 Y”。无法通过身份验证的调用方会收到 401/403,绝不会收到 {"active": false}。
  • 你只能检查自己的凭证。 属于其他人的令牌会显示为非活动状态。因此,此功能适用于代表同一用户运行的网关或 Agent 运行时;它不是多租户验证服务。
  • scope 表示实际有效的权限,而非原始授权内容。 会话令牌不受范围限制,并会报告全部三个账本范围。不要因为授权内容看起来为空,就认为“没有权限”。

以 bio_ 为前缀的字段由我们定义,而非 RFC 7662 规定;其他所有内容均为标准文档内容。GraphQL 中也提供相同操作,名称为 Query.introspectToken。此操作特意不作为 MCP 工具提供,也不会在 RFC 8414 元数据中声明为 introspection_endpoint——原因见 ADR 0017。

在从未配置 OAuth 的部署中,内省仍然有效:API 密钥和会话令牌仍可解析,而 OAuth 令牌会返回 active: false,这在此情况下是正确的。

原生应用链接

此服务始终会响应:

  • GET /.well-known/apple-app-site-association — 为 /ledger/* 上的 io.beancount.ios(团队 PTLM7BZQMM)提供 Apple 通用链接
  • GET /.well-known/assetlinks.json — 为 io.beancount.android 提供 Android 应用链接,并包含 Play 应用签名证书的 SHA-256

两者均指向 Beancount.io 的官方应用,并在 src/features/well-known/api/well-known-route.ts 中硬编码;无需配置。应用仅声明 beancount.io,因此在可自托管的域名上提供相同文件不会关联到任何应用。

OpenAI 应用域名证明

GET /.well-known/openai-apps-challenge 会以 text/plain 返回 OpenAI Plugins 控制面板为 Beancount.io 的应用提交签发的令牌。该令牌不是秘密,因此它是 well-known-route.ts 中的常量,而非环境变量。只有 DASHBOARD_URL 为 https://beancount.io 的部署会提供此令牌;其他所有部署均会响应 404。

MCP Registry 域名证明

可选。MCP_REGISTRY_AUTH_PROOF 保存用于签署 Beancount.io 向官方 MCP Registry 发布内容的密钥的公钥部分 (registry.modelcontextprotocol.io),格式为完整记录 v=MCPv1; k=ed25519; p=<base64 public key>(P-384 密钥的 k=ecdsap384)。 设置后,此服务会使用该值响应 GET /.well-known/mcp-registry-auth, 注册表据此验证 io.beancount/* 下的发布内容是否来自此域名的控制者。若值不是该格式的确切记录, 服务将无法启动。

未设置时 → 404。在可自托管的环境中请留空:在你的域名上提供 Beancount.io 的密钥,会让 Beancount.io 能在你域名的注册表命名空间下发布内容。 清单文件位于此软件包根目录的 server.json;MCP 指南介绍了如何发布。

静态 beancount-mobile 客户端是公开的(无 密钥),只接受授权码和刷新授权,要求使用 S256 PKCE,并且仅注册 io.beancount.ios:/oauth/callback 和 io.beancount.android:/oauth/callback。授权端点还接受 screen_hint=signup,并将其转发到控制面板的 /oauth/mobile-consent 页面,以便用户在应用中点击注册时,打开的是注册页面而不是登录 表单。这只是显示提示,而非策略:此服务无法识别的值会被丢弃而非拒绝,因此版本比自托管服务更新的应用仍会显示登录表单。此提示绝不会转发到 MCP 或 身份客户端交互页面。

OAuth 功能在 GraphQL、REST 和 MCP 上采用统一且封闭的操作矩阵: 读取需要 ledger.read,普通变更需要 ledger.write,删除账本、管理协作者或管理公钥等账本控制平面操作需要 ledger.admin。权限范围具有累积性:ledger.write 包含读取能力,而 ledger.admin 包含写入和读取能力。用户账户生命周期管理不在其中:任何账本权限范围、OAuth 客户端 ID 或账本关系都不能授权 user.delete。

对于账本生命周期、协作者、离开账本和 SSH 公钥操作, ledger.admin 只是委派凭证的权限上限。集中式 PDP 还要求在应用服务/工作流边界处存在当前且完全匹配的 User 或由 Gitea 支持的账本关系。操作的 admin 类别是速率限制和审计元数据,不是额外的授权。关系检查拒绝时会隐藏账本是否存在;关系来源发生故障时会返回服务不可用,且不会执行 Fava/Gitea 变更、Plaid 清理或数据库副作用。现有控制面板和移动端 GraphQL 契约保持不变。

Ledger 数据平面授权

Ledger 元数据、报告、日志、账户、文件、仓库历史记录、BQL、归档、条目、收据和拉取请求,在同一 PDP 中使用与传输方式无关的 ledger.* 操作。GraphQL、REST、MCP 工具和 MCP 资源仍是别名:操作类别保留现有的速率配额,而受保护的服务/工作流会选择规范操作,并在访问或修改数据前评估凭据范围、ledger 固定信息以及当前的 Gitea 所有者/协作者/公开状态事实。

目前公开的 ledger 仍可匿名读取。匿名读取私有 ledger 时会返回需要身份验证;基于关系的已签名拒绝仍会返回可采取后续操作的禁止访问错误。关系数据源故障会返回服务不可用,并作为错误写入审计日志。授权后,文件路径验证、归档名称验证、提交构建、收据所有权/清理以及条目原子性仍是领域不变量。授权决策不会缓存,因此撤销授权或可见性变更会影响下一次调用。

已登录的读者可以就公开或共享的 ledger 提问,包括 UI 请求 Agent 模式时。Agent 会先授权只读的 ai.ledger.ask 操作,然后尝试单独升级为写入操作 ai.ledger.agent。如果升级被拒绝,仍会以只读模式生成回答,不提供编辑工具,也不提供可写 Git 远程;授权数据源故障时仍会默认拒绝访问。

用户域授权

受保护的用户资料、生命周期、API 密钥管理和计费操作,在集中式 TypeScript PDP 中映射为与传输方式无关的 user.* 操作,该 PDP 位于 src/server/api/authorization/。GraphQL、REST 和 MCP 适配器会委托给账户、API 密钥和订阅应用服务;这些服务受保护的公共方法会在读取域数据或执行副作用前做出最终判定。精确的本人关系来自解析后的稳定用户 ID; 撤销 API 密钥时,会从当前数据库行解析所有权,而不会将其复制到元组存储中。

现有凭据约定保持不变。Mutation.deleteAccount 仍不接收参数,并接受浏览器会话和 OAuth 用户凭据,因此修复移动端删除问题无需更改客户端或增加二次验证流程。API 密钥不能删除用户或生成后继密钥。用户资料搜索和更新仍仅限会话使用;资料读取和 API 密钥管理仍遵循原有的权限范围上限。授权后仍会执行付费套餐、权限范围和置顶项收窄、过期时间以及一次性密钥处理规则。自 2026-10-09 起,API 密钥和 SSH 密钥管理、账户删除、套餐等级和用量均不再通过 MCP 提供;这些功能仍由 GraphQL 和 REST 提供(ADR 019 修订案)。

静态套餐配额目录有意设为公开内容,不经过 PDP。订阅状态、结账和门户会话、取消、恢复和升级仍仅限浏览器会话使用;PDP 目录负责管理该凭据规则。op-class.ts 会单独保留所有计费别名在切换前每分钟 300 次的额度,包括公开目录。Stripe 客户绑定、已配置的产品和价格以及订阅所有权仍由支付域检查。关系数据源不可用时,会返回服务不可用,而非策略拒绝,并且会在开始 Stripe 或本地计费操作前返回。现有仪表板和移动端流程无需新增客户端步骤。

没有 OpenFGA 运行时、SDK、服务、新数据库、上下文元组或跨请求授权缓存。authz/model.fga 及其 FGA CLI 测试定义了声明式关系边界;当前数据在本地评估,数据不可用时会默认拒绝。请参阅 authz/README.md 和 ADR 0010。

签名密钥轮换分两个部署步骤进行:替换由密钥管理的 JWKS,部署,然后验证 /api-gateway/oauth/jwks 仅公开新的 kid。被移除密钥签发的令牌会立即停止通过身份验证。切勿将私有 JWK 或令牌放入日志、CI 捕获的命令或已提交的文件中。

刷新令牌撤销会阻止再次刷新,但不会为已签发的自包含访问令牌维护逐请求拒绝列表。这些令牌的最长有效期为一小时(ttl.AccessToken);客户端必须在登出时立即清除本地副本,运维人员则应仅在需要立即全局失效的事件响应中使用签名密钥轮换。

会话有效期

src/features/oauth/data/config.ts 是 OAuth 客户端、受众和有效期策略的唯一来源:包括静态 Mobile 和 Discourse 客户端、MCP 客户端使用的动态注册配置、相对于发行方的受众、令牌和授权有效期,以及刷新令牌轮换。AppConfig.oauth 仅包含部署输入——发行方、交互源、签名密钥和可选的 Discourse 密钥——因此测试或部署不会悄然更改客户端 ID、重定向 URI、受众或 TTL。仪表板浏览器会话不是 OAuth 客户端,因此不会出现在此目录中。

这些资源值是 OAuth 标识符,而非 HTTP 挂载路径。GraphQL 和 REST 保留现有传输 URL;历史遗留的 <issuer>/v1 标识符指向它们共用的授权边界,尽管该 URL 上并未挂载任何端点。此标识符保持稳定,因为已发布的原生客户端和持久化刷新授权均绑定到该确切受众。

客户端/配置文件注册方式令牌端点处的凭据资源受众
Beancount Mobile静态公共客户端 + PKCE应用 API(<issuer>/v1)
Discourse 论坛静态client_secret_basic + PKCE仅 UserInfo(无 API 资源)
MCP/Agent 集成DCR 或 CIMD已注册或已发布的元数据(通常为公共客户端 + PKCE)MCP(<issuer>/api-gateway/mcp)
凭据有效期说明
访问令牌(所有客户端)1 小时自包含;撤销无法使其提前失效。
刷新令牌(原生应用)365 天每次刷新时都会完整重新签发。
刷新令牌(MCP 主机)45 天公共客户端每次刷新时都会轮换。
刷新令牌(Discourse)30 天oidc-provider 的默认轮换策略。
授权(原生应用)会话时长 + 1 天每次刷新时向后延长。
授权(MCP 主机)46 天,≤ 1 年刷新时延长;最长不超过用户同意后 1 年。
授权(Discourse)14 天不延长。
授权服务器会话14 天浏览器 SSO Cookie,而非应用会话。

原生应用会话采用的是空闲时长,而非固定期限。每次刷新时,其刷新令牌都会轮换,背后的授权也会以完整的新有效期重新保存。因此,在此时长内至少使用过一次的设备会一直保持登录状态;若设备在整个时长内都未使用,则必须在系统浏览器中重新授权。这是有意为之:oidc-provider 只会在授权时写入 Grant,并会拒绝任何授权已过期的刷新请求。因此,真正限制会话时长的是固定的授权有效期,而非刷新令牌的有效期;此外,其默认轮换策略也会停止轮换超过 365.25 天的令牌链。这两项默认行为都由 features/oauth/data/config.ts 中的策略函数覆盖。这些时长是目录中经过审核的代码值,而非环境变量,因为不同部署不应在凭据有效期上出现不易察觉的差异。

MCP 主机的连接(任何 DCR 或 CIMD 客户端)采用相同方式,但有两点不同(ADR 019 D5)。其空闲时长为 45 天——记账连接器通常按月结账,而 30 天的时长会导致结账间隔为 31 天的用户授权过期。此外,连接还需要每年重新批准:授权在每次刷新时都会延长,但最长不超过用户同意后 1 年,因为这些主机会在自己的服务器上保存具有账本写入权限的刷新令牌。公共客户端的刷新令牌无论何时都在每次刷新时轮换,这是 MCP 规范的要求(发送方约束令牌是唯一例外)。Discourse 保留 oidc-provider 的默认设置。

提前结束长期会话的方式是撤销:注销会撤销刷新凭据,并连带撤销授权。 密码重置会一次性终止所有这些会话。在更改密码的同一事务中,它会删除所有仪表板和 CLI 会话(jwts 行),并撤销该用户在每个客户端持有的所有 OAuth 授权、刷新令牌和授权服务器会话(oauthGrant.revokeAllForUser)。如果无法撤销这些会话,重置就会失败,旧密码会保持不变。已签发的访问令牌会在一小时内过期,且无法刷新。个人 API 密钥(bcio_)和 SSH 密钥不会被撤销:它们是用户有意创建的长期凭据,并且都会列在仪表板的 Settings → Personal access tokens 和 Settings → SSH Keys 下,用户可在那里撤销。

在这些有效期规则上线前签发的凭据会按照原有规则继续生效:现有刷新令牌在其原始到期时间前仍会被接受,而它轮换生成的新令牌则采用新的有效期窗口,因此已安装的应用会在首次刷新时自动迁移——无需重新授权,也不会强制注销。已经过期的授权仍会被拒绝;更长的有效期不会让已失效的授权恢复有效。

Git over SSH 代理(可选,ADR 0004)

Backend-v2 可以自行通过 SSH 提供 Git 服务,而不使用 Gitea,这样仅允许主分支的规则和所有应用层检查也会同时应用于 SSH 和 HTTP。它会根据 Gitea 持有的密钥对客户端进行身份验证,然后使用该用户自己的凭据与 Gitea 通信,通过 HTTP 执行 Git 操作——我们不会在任何地方存储或注册自己的密钥。只有同时设置了 SSH_PROXY_ENABLED 和 SSH_PROXY_HOST_KEY 时,此功能才会启用:

SSH_PROXY_ENABLED=false
SSH_PROXY_PORT=2222
SSH_PROXY_HOST_KEY=
免费层级指令数量上限(ADR 0005)

代理还会强制执行免费层级的指令数量上限,取代最后一个 Gitea pre-receive hook。它会询问 ledger-v2 当前账本中的计数,并拒绝已超出上限时发起的推送——它_无法_回答的问题是推送是否会使计数超出上限,因为精简 pack 不包含对象存储。

此功能没有开关。发生任何错误时,它都会允许通过:hook 移除后,超出上限的用户只能通过应用删除条目来恢复正常,因此检查失败时若拒绝通过,就会同时锁死两条路。

SSH_PROXY_HOST_KEY 应保存 Gitea 现有的主机私钥。如果在客户端以前连接过的端口上提供了新密钥,就会出现 REMOTE HOST IDENTIFICATION HAS CHANGED——这与攻击行为无法区分——而 Git 会拒绝继续。使用 _infra/print-ssh-host-key.sh 获取密钥;这是私钥,因此只能存放在未提交的 .env 中。

在 Windows 上运行

我们建议使用适用于 Linux 的 Windows 子系统(WSL)。安装 WSL 后,打开终端并按照相同的安装步骤操作。如果你更喜欢使用 PowerShell,请确保在运行命令前已准备好 nvm 和 Git Bash。

开发

启动服务

要在开发模式下运行服务:

yarn start

服务将在配置指定的端口上运行(默认:4104)。

可用脚本
  • yarn start:使用 ts-node 启动服务
  • yarn build:编译服务
  • yarn dist:start:使用 Node.js 启动编译后的服务(需要先执行 build)
  • yarn typecheck:检查 TypeScript 类型
  • yarn test:运行单元测试
  • yarn lint:运行 ESLint,检查并修复代码风格问题
  • yarn kill:终止占用端口 4104 的 node 服务
  • yarn mcp:conformance <base-url>:检查部署的 MCP 端点是否可连接(参见连接 MCP 客户端)
  • yarn mcp:agent-eval:通过真实的 Claude Code 和 Codex 会话,针对托管的 MCP 端点运行入门流程,并评估回答和账本影响(会产生费用,需主动选择加入;参见 MCP Agent 测试流程)
作业调度器

后台作业使用 node-cron 进行调度,并在服务器启动时自动运行:

  • JWT 清理:每天午夜(00:00)运行,从 PostgreSQL 中删除已过期的令牌
  • 身份验证邮件:每五秒运行一次,每批处理最多五个持久化发件箱作业
  • 开发测试作业:在开发模式下每 5 分钟运行一次(console.log 测试)

实现详情请参阅 src/scheduler/,有关添加新作业的信息请参阅 src/scheduler/README.md。

API 文档

后端通过 Apollo Server 提供 GraphQL API。运行服务器并访问 GraphQL playground,即可查看 API 架构。

通过 GraphQL createLedger、REST POST /api-gateway/v1/ledgers 或 MCP createLedger 创建账簿时,默认使用 STARTER 模板(模板未提供或为 null 时也一样)。Starter 包含选项和账户脚手架,不含交易或期初余额。若要创建填充了示例数据的账簿,请选择 SAMPLE。模板更改仅适用于新创建的账簿;不会重写现有账簿。

与 OpenAI 兼容的模型代理

POST /api-gateway/ai/openai/chat/completions 接受 max_completion_tokens 或旧版 max_tokens,其值必须为正整数,并会原样转发提供的字段。若同时提供这两个字段,则请求无效。若两者均未提供,代理会发送 max_completion_tokens: 1500,而不是依赖提供商未设上限的默认值。显式设置的上限会替代此默认值;但仍受所选模型的限制。

此路由要求使用具有写入权限的已认证凭据,并会在联系模型前检查调用方每月的 AI 额度。调用成功后会记录提供商报告的总令牌用量。此兼容协议的端点保留了文档中说明的 GraphQL 和 MCP 传输例外。

识别出的提供商配额拒绝会返回 HTTP 429,并包含 error.code: "RATE_LIMITED" 和 error.metadata.quotaScope: "shared_service"。此限制属于部署的共享模型容量;调用方每月的 AI 额度会单独跟踪。如果提供商给出了有效的未来重置时间,blockedUntil 会包含其 UTC ISO 时间戳,消息中也会包含该时间。可选的 retryAfter 是以秒为单位的正整数,按以下顺序取自提供商的数字字段、整数型 Retry-After 标头或重置时间戳。失败的请求不会记录令牌用量。

系统会通过有限层级的 error/message 包装结构识别提供商配额文档。其他提供商故障会保留其 HTTP 状态码,并返回通用产品错误;提供商消息、链接和凭据绝不会复制到该错误中。目前,每月用量 API 不会报告共享容量。

公开账簿站点地图

匿名访问的 GET /api-gateway/sitemap.xml 端点会列出公开账簿页面。该站点地图操作指南说明了完整的分页、缓存和故障行为、规范 URL 以及部署后的验证。

连接 MCP 客户端

后端在 /api-gateway/mcp 提供无状态的 Streamable HTTP 端点。使用针对该资源的 OAuth 授权,或 bcio_ API 密钥进行连接。Ledger 工具接受 ledger: "owner/name";绑定到特定 ledger 的凭据默认使用其绑定的 ledger,且无法选择其他 ledger。未绑定的凭据必须在每次调用时选择 ledger;账户工具无需指定 ledger。

{
  "mcpServers": {
    "beancount": {
      "type": "http",
      "url": "https://your-deployment/api-gateway/mcp",
      "headers": { "Authorization": "Bearer bcio_your_ledger_scoped_key" }
    }
  }
}

Beancount.io 的托管端点已列入官方 MCP Registry,名称为 io.beancount/beancount,因此支持 Registry 的客户端可以从列表中添加该端点并通过 OAuth 登录;请参阅从 MCP Registry 添加。

每个结果都包含易读的文本块和类型化的 structuredContent;每次失败都会返回 {code, message, hint} 并将 isError 设为 true,因此 Agent 可以根据代码而不是描述文字进行分支处理。

runBqlQuery 会报告单次查询执行得到的规范化行,同时保留原始字符串结果。多行单元格和指令都会计为结果行。如果 ledger 服务未提供行数,易读文本会显示 row count unavailable。文本和结构化查询行为请参阅结果封装。

对于空的摘要说明,请读取 beancount://{owner}/{name}/narration-transactions?narration=。交易查询示例说明了显式值和保留的非空路径别名。

资产负债表中的本期收益示例展示了 Codex 和 Claude Code 使用相同读取 URI 的方式,包括仅用于报告的权益结转和账户筛选范围。

Beancount.io MCP 指南介绍了设置、OAuth 和 API 密钥权限、请求如何到达 ledger、全部 43 个工具和 64 个资源模板、四个会计提示词及 Claude Code 和 Codex 如何使用这些提示词、结果封装及其错误代码、如何将指令编写为 Beancount 文本、文件编辑预览、银行导入、协议示例和部署诊断。指南还记录了一致性检查的局限,以及当前面向客户端的功能与 REST 的差异。

若想了解真实编码 Agent 如何处理这套功能,MCP Agent 使用流程通过 Claude Code 和 Codex 执行三个入门任务,检查回答和之后的 ledger 状态,并记录调用次数、轮数、时间和成本。更改工具、资源、提示词或服务端指令前后,都应运行此流程。

设计决策请参阅 ADR 0007(传输契约)和 ADR 0008(工具、资源和功能表面一致性)。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。