Actual Budget 25.4.0 版本解析:Pluggy.ai 巴西银行同步、内嵌同步服务器与移动端预算体验升级
Actual Budget 25.4.0 版本解析:Pluggy.ai 巴西银行同步、内嵌同步服务器与移动端预算体验升级
Actual Budget(本地优先的个人财务管理应用)于 2025 年 4 月 3 日发布了 25.4.0 版本,Docker 镜像标签为 25.4.0。本版本围绕三大主线展开:一是通过 Pluggy.ai 为巴西银行提供实验性的银行同步能力,二是将同步服务器(sync-server)实验性地嵌入 Electron 桌面应用,三是围绕移动端预算页、交易列表与日期国际化做了一系列体验增强。本文以官方发布文档(packages/docs/blog/2025-04-03-release-25-4-0.md)为骨架,结合仓库源码逐项拆解这些能力的实现细节与使用方式。
核心特性(Features)
1. Pluggy.ai:巴西银行同步的实验性支持
25.4.0 首次引入了面向巴西银行的 Pluggy.ai 银行同步(PR #4049)。这是一项实验性能力,需要在同步服务器侧完成凭据配置后才能使用。
从源码结构看,Pluggy.ai 集成位于 packages/sync-server/src/app-pluggyai/ 目录,包含两个文件:
app-pluggyai.js:负责定义 Express 路由(/status、/accounts、/transactions),并完成权限校验、文件访问控制与数据归一化;pluggyai-service.js:封装对pluggy-sdk的调用,负责创建 Pluggy 客户端、拉取账户与交易。
凭据配置与三级路由设计
pluggyai-service.js 中的 getCredentialSource(fileId) 定义了凭据的两种来源:
function getCredentialSource(fileId) {
if (!!fileId && hasCredentials(fileId)) {
return 'per-budget-file';
}
if (hasCredentials()) {
return 'global';
}
return null;
}
per-budget-file:为单个预算文件单独配置的 Pluggy 凭据(clientId、clientSecret、itemIds);global:全局共享的 Pluggy 凭据,适用于所有预算文件。
hasCredentials() 要求三个密钥(pluggyai_clientId、pluggyai_clientSecret、pluggyai_itemIds)同时存在才认为已配置,这些密钥通过 secretsService 读写,并缓存在内存中的 pluggyClients Map 中(以文件 ID 与凭据序列化为缓存键)。itemIds 以逗号分隔存储,解析时会被拆分、去空格并过滤空项。
三个路由的职责与鉴权
app-pluggyai.js 挂载了 requestLoggerMiddleware、express.json() 与 validateSessionMiddleware,并统一从请求头 X-Actual-File-Id 读取文件 ID,通过 canAccessFile()(管理员或对该文件有访问权的用户)校验权限,非法文件 ID 返回 400 invalid-file-id,无权限返回 403 file-access-denied:
POST /status:返回当前文件是否已配置 Pluggy 及凭据来源(configured与source);POST /accounts:遍历所有itemIds调用pluggyaiService.getAccountsByItemId()聚合账户列表;若未配置凭据返回400 not-configured;POST /transactions:接收{ accountId, startDate },拉取交易并归一化为 Actual 的转账格式,同时返回起始余额(startingBalance)与余额对象(balances)。
交易归一化的关键逻辑
/transactions 路由中的归一化逻辑值得注意:
- 交易按
booked(已入账)与pending(待入账)分组,判断依据是trans.status === 'PENDING'; - 早于
startDate且非沙箱的交易会被跳过; - 信用卡(
account.type === 'CREDIT')账户的金额与余额会被取反,以符合 Actual 的记账方向; - 交易日期通过
getTransactionDateCorrected()修正:若交易携带信用卡分期元数据(creditCardMetadata.installmentNumber),则基于purchaseDate按月数递推分期日期,并使用addMonthsClamped()防止 31 日跨月溢出; - 交易对象经
flattenObject()扁平化后与归一化字段合并; - 收款方(payee)名称优先取
merchant.name/merchant.businessName,其次按借贷方向取paymentData.receiver或paymentData.payer的名称或证件号(documentNumber.value); - 所有交易按
sortOrder(交易日期的时间戳)降序排列,返回all、booked、pending三组结果。
沙箱账户的兼容处理
pluggyai-service.js 中 getTransactionsByAccountId() 对 Pluggy 沙箱账户做了特殊处理:沙箱数据不会自动推移日期,交易往往超过 90 天,因此当账户 owner 为 John Doe 时,将 startDate 强制设为 2000-01-01 并给交易打上 sandbox: true 标记,以便测试时能取回全部历史数据。25.4.0 还修复了 Pluggy.ai 信用卡交易金额反相(#4628)与外币交易(#4712)等问题。
2. 账户新增 "上次核对时间"(last reconciled)
PR #4459 为账户模型增加了 last_reconciled 时间戳字段。对应的数据库迁移文件为 packages/loot-core/migrations/1740506588539_add_last_reconciled_at.sql:
BEGIN TRANSACTION;
ALTER TABLE accounts ADD COLUMN last_reconciled text;
COMMIT;
在服务端,packages/loot-core/src/server/accounts/app.ts 的账户查询中新增了 last_reconciled 字段,并将其纳入账户实体的返回结果(dbAccount.last_reconciled ?? null),同时支持在账户更新时回写该字段。桌面客户端侧,账户头部(Header.tsx)、对账弹窗(AccountReconcileModal.tsx)与对账组件(Reconcile.tsx)均读取该字段,用于展示账户最近一次完成对账的时间,帮助用户跟踪对账节奏。
3. 实验性:将同步服务器嵌入桌面应用
PR #4526 是 25.4.0 的另一项重要实验:将 sync-server 直接嵌入 Electron 桌面应用,使用户无需单独部署 Actual Server 即可在同一进程体系中获得完整的服务端能力。
从 packages/desktop-electron/index.ts 的实现看,桌面应用通过 utilityProcess.fork() 启动 sync-server 的构建产物(@actual-app/sync-server/package.json 所在目录下的 build/app.js),并通过环境变量完成配置注入:
const envVariables: Env = {
...process.env, // required
ACTUAL_PORT: `${syncServerConfig.port}`,
ACTUAL_HOSTNAME: `${syncServerConfig.hostname}`,
ACTUAL_SERVER_FILES: `${syncServerConfig.ACTUAL_SERVER_FILES}`,
ACTUAL_USER_FILES: `${syncServerConfig.ACTUAL_USER_FILES}`,
ACTUAL_DATA_DIR: `${syncServerConfig.ACTUAL_SERVER_DATA_DIR}`,
ACTUAL_WEB_ROOT: webRoot,
};
其中 ACTUAL_SERVER_DATA_DIR 是 sync-server 的数据根目录,服务端文件(server-files)与用户文件(user-files)均在其下;ACTUAL_WEB_ROOT 指向 @actual-app/web 的构建目录,由 web 客户端提供界面。桌面端通过 IPC 暴露了 start-sync-server、stop-sync-server、is-sync-server-running 三个通道(见 preload.ts),分别用于启动、停止与查询内嵌服务状态,子进程的 stdout/stderr 会转发到主窗口日志中。该功能在开发模式下还会为 fork 的子进程附加 --inspect 以便调试。
使用方式与前置条件:该特性属于实验性能力,需自行构建或使用支持此功能的桌面端版本;内嵌服务的数据目录、端口等可通过环境变量(ACTUAL_DATA_DIR、ACTUAL_PORT 等)控制,与独立部署 sync-server 时一致。同一版本还配套修复了 sync-server.Dockerfile 未正确构建客户端的问题(#4677)。
体验增强(Enhancements)
日期与日期选择器的国际化翻译
PR #4211 为日期与日期选择器(datepicker)增加了按所选语言翻译的能力,将日期控件从硬编码英文切换为 i18n 驱动的本地化文案。25.4.0 同时收录了 PR #4635,为规则(rules)与字段增加了更多翻译条目,规则编辑器中的条件、操作与字段名在切换语言后能够同步本地化。
移动端预算页的 Banner 提示
PR #4643(重做)为移动端预算表引入了 Banner 机制,用于主动提醒用户各类预算待办事项。实现位于 packages/desktop-client/src/components/mobile/budget/BudgetPage.tsx,共实现三类 Banner:
- OverbudgetedBanner(超预算警告,
type="critical"):当toBudget金额为负时显示 "You have budgeted more than your available funds",提供 Cover 按钮打开cover弹窗,从其他类别或 "To Budget" 中移入资金覆盖超预算部分; - OverspendingBanner(超支类别警告,
type="critical"):通过useOverspentCategories()统计超支类别,显示超支类别数量,提供覆盖(cover-overspending)与类别选择弹窗(category-autocomplete),且支持显示隐藏的超支类别(#4680); - UncategorizedTransactionsBanner(未分类交易提醒,
type="warning"):统计未分类交易条数与总额,提供 Categorize 按钮跳转至/categories/uncategorized。
Banner 的配色随主题变量变化(errorBackground、warningBackground、noticeBackground),在无相关事项时返回 null 不占位,避免干扰正常浏览。
移动端交易与账户操作增强
- 子交易搜索(#4471):移动端支持搜索子交易(split 中的条目),方便在拆分交易中快速定位明细;
- 拖拽排序(#4476、#4482):移动端支持拖拽重排账户列表与预算页的收入类别,目前仅支持 Chromium 内核浏览器;
- 标记转账(#4511):小屏设备上允许将交易标记为转账(Transfer);
- 拆分交易(#4536):当拆分中不存在空条目时,交易编辑按钮显示 "Add New Split" 文案;
- 目标与预算信息展示(#4546):移动端展示类别的目标(goal)与预算信息;
- 收入类别余额跳转(#4621):点击收入类别余额可查看对应交易列表;
- 通知滑动关闭(#4606):左右滑动即可关闭通知提醒;
- 搜索/过滤时拆分交易折叠(#4600):带搜索或过滤条件时支持折叠/展开拆分交易。
银行同步相关增强
- 最后同步时间人性化(#4523):银行同步的最后时间以自然语言(如 "2 hours ago")展示,取代原始时间戳;
- 有限历史银行名单扩充(#4416):将
INDUSTRIEL_CMCIFRPAXXX、QONTO_QNTOFRP1加入BANKS_WITH_LIMITED_HISTORY常量(这些银行仅提供约 90 天交易历史);同期 #4595 将捷克 AirBank 加入同类名单; - 删除交易不再被重复导入(#4644):新增配置项,控制银行同步时是否重新导入已删除的交易;
- 机构列展示(#4646):关联账户弹窗中的账户名称旁增加机构(institution)列;
- 外部账户记忆化(#4540):对银行同步弹窗中的外部账户列表做 memoize,减少重复请求;
- 同步摘要月度平均改进(#4302):改进汇总报表(summary report)的月度平均计算方式。
关键 Bugfix 一览
25.4.0 修复了大量与同步服务器、OpenID、移动端交互相关的问题,值得运维与重度用户关注:
同步服务器与 OpenID 配置
ACTUAL_OPENID_DISCOVERY_URL新变量(#4530)与ACTUAL_OPENID_ENFORCE强制启用修复(#4533);- 生产构建缺失
convict与pluggy-sdk依赖(#4534)、默认数据目录配置(#4535)、ACTUAL_PORT/PORT变量处理(#4537)、config 文件中的对象类型支持(#4688)、OpenID 响应缺失iss参数(#4626); - 这些修复与 packages/sync-server/src/load-config.js 中基于
convict的配置体系(ACTUAL_*环境变量、config.json加载、tokenExpiration自定义格式校验)直接相关。
银行同步
- mBank 波兰交易日期修复(#4493)、SimpleFIN 批量同步超时与调试日志(#4515)、银行同步 API 错误收集修复(#4689)、GoCardless 机构结构差异导致
SelectLinkedAccounts失效(#4667)。
规则与交易
- 排程无 payee 时自动补充 "payee is nothing" 规则条件(#4267)、规则创建拆分交易失败(#4623)、
scheduleIsRecurring空值判断(#4627)、带模板规则保存失败(#4686)、转账交易过滤(#4519)。
移动端与界面
- iOS 上预算金额聚焦(#4651)、移动端远程预算文件切换(#4596)、自定义报表 "any" 条件保存(#4652)、报表仪表盘现金流组件过滤(#4683)、service worker 缓存上限提升至 5MB(#4665)。
维护与架构演进(Maintenance)
25.4.0 背后包含大量架构级维护工作,这些改动虽不直接面向用户,但为后续版本奠定了更清晰的分层结构:
- Redux Toolkit 迁移第二阶段(#4114、#4119、#4126、#4127、#4128):budgets、modals、notifications、preferences、users 等 slice 相继迁移至 Redux Toolkit;
- 服务端 handler 拆分(#4442、#4443、#4649、#4661):将预算类别、payee、spreadsheet、同步相关的服务端 handler 从庞大的
main.ts中抽取到独立的server/*/app.ts,并统一使用实现函数类型,减少类型重复(#4650); - 组件库收敛(#4554、#4556、#4563、#4568):图标文件、
Select、Input、useResponsive等组件与 Hook 迁入@actual-app/components组件库,移除废弃的MenuButton、Button组件; - 依赖与构建治理(#4592、#4609、#4610、#4641、#4654、#4656、#4717):loot-core 路径解析迁移至 yarn workspaces、数据模型导入统一走 barrel 文件、yarn 升级至 4.7.0、axios 升至 1.8.3、tar-fs 升至 2.1.2、构建禁用名称混淆(#4532);
- 类型系统强化(#4248、#4249、#4542、#4524):
db.first、db.firstSync、db.all泛型化,预算表组件类型细化; - 发布流程自动化(#4664):新增 release note 生成器,后续版本发布说明可由脚本自动生成,这正是本文所依据的发布文档的产出渠道之一。
升级与验证建议
- 使用 Docker 部署 Actual Server 的用户可拉取
actualbudget/actual:25.4.0镜像完成升级;last_reconciled字段通过迁移脚本自动添加,无需手动改库; - 尝试 Pluggy.ai 巴西银行同步的用户,需在同步服务器中配置
pluggyai_clientId、pluggyai_clientSecret、pluggyai_itemIds三组密钥(支持全局或按预算文件),并通过POST /status验证配置是否生效; - 关注 OpenID 配置的运维人员,本版本对
ACTUAL_OPENID_DISCOVERY_URL、ACTUAL_OPENID_ENFORCE、ACTUAL_PORT等环境变量的行为做了多项修复,建议升级后回归验证登录与端口绑定; - 桌面端用户可在设置中体验内嵌同步服务器(实验性),注意其数据目录与独立部署版本相互独立,迁移数据时需分别处理。
25.4.0 是 Actual Budget 在"银行同步生态拓展 + 桌面端架构收敛 + 移动端体验打磨"三线并进的一次集中发布:Pluggy.ai 的实验性接入填补了巴西银行同步的空白,内嵌同步服务器为单机用户提供了更完整的本地优先方案,而预算 Banner、日期翻译与大量移动端交互改进则显著降低了日常记账的摩擦成本。