Actual 23.4.2 版本解析:按命名计划自动预算、侧边栏浮动优化与服务端密码重置脚本
【免费下载链接】actualA local-first personal finance app项目地址: https://gitcode.com/GitHub_Trending/ac/actual
Actual 是一款本地优先(local-first)的个人财务管理应用,23.4.2 是其 2023 年 4 月发布的一个维护型版本,涵盖客户端(Actual)与服务端(Actual Server)两侧的改动。本文以官方发布说明 packages/docs/blog/2023-04-22-release-23.4.2.md 为主线,结合当前仓库源码,深入讲解「按命名计划(named schedules)自动写入预算模板」「侧边栏自动浮动行为改进」「撤销快捷键修复」「零值对账修复」「自动补全依赖清理」以及服务端「reset-password密码重置脚本」与「Nordigen 错误信息增强」等核心变更的实现原理与使用方式,帮助读者理解这些更新背后的工程逻辑并直接上手实践。
版本概览:一个客户端与服务端同步发布的维护版本
23.4.2 同时发布了客户端与服务端两个组件,Docker 镜像标签为23.4.2:
| 组件 | 版本 | 变更类型 |
|---|---|---|
| Actual(客户端) | 23.4.2 | 1 个功能(Feature)、1 个增强(Enhancement)、2 个缺陷修复(Bugfix)、2 项维护(Maintenance) |
| Actual Server(服务端) | 23.4.2 | 1 个功能、1 个增强 |
整体上这是一次小版本迭代:客户端聚焦预算模板与交互体验的打磨,服务端则补上了一个此前缺失的运维能力——命令行重置密码。下文分别展开。
客户端(Actual 23.4.2)变更详解
按命名计划自动生成预算模板金额(Feature #885)
本版本最重要的功能更新是:预算模板(Budget Template)新增了schedule关键字,可以按「命名计划」(named schedule)自动计算出当月应预算的金额,该功能由社区贡献者 pole95 提交。
在 Actual 的预算模板体系中,每个模板行由类型(type)、指令(directive)、优先级(priority)等字段构成。schedule类型的模板行并不直接写死金额,而是通过name(或内部使用的scheduleId)关联到一条已有的计划(Schedule),由预算引擎在运行时解析出金额。相关实现位于 packages/loot-core/src/server/budget/schedule-template.ts,入口函数为runSchedule。
从源码可以看到其核心计算流程:
- 按名称/ID 匹配计划:通过
template.scheduleId(优先,避免重命名破坏关联)或template.name(兼容旧数据)查询schedules表中tombstone = 0的活跃计划,再经由getRuleForSchedule拿到该计划关联的规则,从中提取日期条件与金额条件。 - 计算单次金额:从规则条件中提取金额,若金额条件是区间(
isbetween),则取两端平均值;若模板配置了adjustment(调整量)与adjustmentType,还会按百分比(percent)或固定值(fixed)对金额做修正——例如adjustment: 10, adjustmentType: 'percent'表示在原金额基础上上浮 10%。 - 区分「当月付清」与「沉没分摊」:对于本月到期的计划(pay-month-of,即
num_months === 0且频率与间隔满足条件,如每月一次、每周间隔 ≤ 4、每天间隔 ≤ 31),直接预算全额;对于更远的未来计划(如年度账单),则按getMonthlyBaseContribution/getSinkingContributionBreakdown进行跨月分摊(sinking fund),将目标金额按月拆解,并优先用已有余额覆盖最近到期的计划。 - 处理边界情况:已完成的计划(
completed)会被排除;已经过去的非重复计划会记录Schedule xxx is in the Past错误;周/日频率且间隔较大的计划按跨月月数折算。
对应的单元测试覆盖了这些行为,见 packages/loot-core/src/server/budget/schedule-template.test.ts,例如:
- 月度重复计划直接预算全额(金额为负时取正,如
-10000→ 预算10000); - 年度计划配合已有余额时仅预算当月分摊额(
$1200的年度账单当月只需$1000); full: true(到期月一次性付清)时不为未来月份预存资金;percent/fixed两种调整方式分别得到11000与10500的结果;- 多计划并存时按到期日排序,让已有余额优先覆盖最早的账单。
使用方式:在预算模板中新增一行,type 设为schedule,name 填写要关联的计划的名称(或使用内部 scheduleId),引擎即会在每次运行模板时自动按计划金额与日期计算应预算值,无需手工维护金额,计划金额变动时模板自动跟随。
改进侧边栏自动浮动行为(Enhancement #868)
该版本优化了侧边栏的「自动浮动」(auto-floating)表现。在桌面端中,侧边栏支持浮动模式(floating sidebar),相关组件位于 packages/desktop-client/src/components/sidebar,其中FloatableSidebar在 packages/desktop-client/src/components/FinancesApp.tsx 中被挂载。
侧边栏的浮动行为同时受两个因素控制(见 packages/desktop-client/src/components/Titlebar.tsx):
- 用户偏好
floatingSidebar(通过useGlobalPref读取); sidebar.alwaysFloats(来自 packages/desktop-client/src/components/sidebar/SidebarProvider.tsx 的上下文状态)。
改进后的自动浮动逻辑让侧边栏在窗口宽度变化或用户展开/收起操作时,能更平稳地在「固定」与「浮动」两种形态之间切换,减少跳动与遮挡,提升多窗口、窄屏场景下的使用体验。这是交互层的小步优化,不影响数据与预算逻辑。
修复撤销键盘快捷键失效问题(Bugfix #926)
此前在某些场景下,Ctrl+Z(macOS 为Cmd+Z)撤销快捷键会被忽略,本版本修复了该问题。
Actual 的撤销机制是「本地优先 + 服务端同步」架构下的关键能力:撤销/重做并非简单操作 DOM 或组件状态,而是通过向核心层发送undo/redo消息、回滚底层数据表变更实现的。相关实现见:
- packages/desktop-client/src/undo/index.ts:核心入口,
_undo = throttle(() => send('undo'), 100)对undo消息做了 100ms 节流,并通过_undoEnabled标志在特定场景下临时禁用撤销; - packages/desktop-client/src/hooks/useUndo.ts:将撤销能力以 hook 形式注入页面组件;
- packages/desktop-client/src/global-events.ts:监听
undo-event,将撤销事件与标签(undoTag)状态路由到各页面监听器。
修复前,当焦点位于某些输入控件或撤销状态被节流窗口吞掉时,快捷键事件可能无法正确触发undo();修复后撤销快捷键在更多交互场景下都能稳定响应,配合界面右下角的撤销提示条(undo notification)使用体验更一致。
修复零值预算的对账问题(Bugfix #915)
该修复针对「对一个零值预算执行对账(reconcile)」时出现的异常。对账是 Actual 核对账户余额与账单一致性的核心功能,其实现位于 packages/loot-core/src/server/accounts/app.ts,其中reconcileTransactions负责将银行同步/导入的交易与现有交易重新匹配(相关逻辑也有测试覆盖,见 packages/loot-core/src/server/accounts/sync.test.ts)。
修复前,当对账涉及的金额合计为零(例如收支相抵或全部金额为 0 的批量操作)时,会触发除零或空值比较等边界问题;修复后引擎能正确处理零值场景,保证对账过程正常完成、余额状态一致。
自动补全依赖清理(Maintenance #916 / #924)
两个维护性 PR 完成了对旧自动补全(autocomplete)组件的收尾:
- #916:移除
@jlongster/lively依赖,重构旧的自动补全组件使其不再依赖该库,并禁用了新的自动补全; - #924:进一步移除
react-select及新自动补全组件。
这说明 23.4.2 处在自动补全组件的新旧交替期:旧实现被彻底清理,新实现(由后续版本基于组件库 packages/component-library 中的Select等组件重写)暂时禁用。对普通用户而言,此次清理减少了打包体积与运行时依赖,属于纯工程性改进,不改变功能行为。
服务端(Actual Server 23.4.2)变更详解
新增npm run reset-password脚本(Feature #186)
这是本版本服务端最重要的运维能力补充:通过一条命令即可设置或重置服务器登录密码,解决了此前忘记密码后只能手工操作数据库的痛点。
使用方式(在 sync-server 目录下执行):
npm run reset-password脚本会自动判断当前状态并走两条分支:
- 尚未设置过密码(首次初始化):提示
It looks like you don't have a password set yet. Let's set one up now!,交互式输入新密码后完成初始化; - 已设置过密码:提示
It looks like you already have a password set. Let's reset it!,交互式输入新密码后完成重置,并提醒所有已登录的浏览器与设备需要使用新密码重新登录。
脚本实现见 packages/sync-server/src/scripts/reset-password.js,它通过needsBootstrap()判断是否首次初始化,分别调用 packages/sync-server/src/accounts/password.js 中的bootstrapPassword与changePassword。
除了npm run reset-password,服务端 CLI 还提供了等价的命令行参数形式(见 packages/sync-server/bin/actual-server.js):
actual-server --reset-passwordbin/actual-server.js通过 Node 内置的parseArgs解析--reset-password布尔参数,命中后先动态加载../src/scripts/reset-password.js执行重置,随后process.exit(),不会启动同步服务。
底层实现原理:密码并非明文存储,而是以 Argon2id 哈希保存。hashPassword使用如下参数(见 packages/sync-server/src/accounts/password.js,参数遵循 OWASP 密码存储建议):
const ARGON2_OPTIONS = { type: argon2.argon2id, // Argon2id 变体 memoryCost: 47104, // 内存成本 46 MiB timeCost: 1, // 迭代次数 parallelism: 1, // 并行度 };同时兼容旧数据:verifyPassword会先判断哈希前缀,$argon2开头走 Argon2 校验,否则回退到 bcrypt 比较(bcrypt.compare),保证升级前用 bcrypt 存储的密码仍可正常登录;登录成功时还会将旧 bcrypt 哈希原地升级为 Argon2id 哈希(见loginWithPassword中的isLegacyHash分支)。
两个关键安全细节:
- 空密码被拒绝:
isValidPassword拒绝null、undefined与空字符串,bootstrapPassword与changePassword对空密码统一返回{ error: 'invalid-password' }; - OpenID-only 实例保护:如果实例启用了 OpenID 认证(没有 password 认证方法),
changePassword会返回{ error: 'no-password-method' }而不是静默成功,且不会禁用已存在的 OpenID 方法——避免误操作破坏已有认证配置。这两点均有测试验证,见 packages/sync-server/src/accounts/password.test.js。
服务端 CLI 的完整参数表(见 packages/sync-server/README.md)如下:
| 参数 | 说明 |
|---|---|
-h/--help | 打印帮助信息并退出 |
-v/--version | 打印版本号并退出 |
--config | 指定配置文件路径 |
--reset-password | 设置或重置服务器密码 |
注意:@actual-app/sync-servernpm 包要求Node.js v22 或更高版本(见 packages/sync-server/README.md)。
更清晰地报告 Nordigen 请求异常(Enhancement #189)
Nordigen(现 GoCardless Bank Account Data)是 Actual 对接银行账户数据聚合的渠道之一。此前当 Nordigen 请求返回非预期状态码时,报错信息较为模糊,难以定位问题;本版本增强了错误报告,让失败请求的状态码与上下文信息更明确,便于用户在银行同步失败时快速判断是凭证失效、账户被移除还是 API 侧限流。
仓库中 Nordigen/GoCardless 相关实现位于 packages/sync-server/src/app-gocardless(共 80 余个 TS 文件),涉及账户授权、交易拉取、银行元数据等完整流程。该增强不改变同步功能本身,只提升可观测性,属于服务端的可维护性改进。
小结
Actual 23.4.2 是一个「稳中有进」的版本:
- 客户端:为预算模板带来按命名计划自动预算的能力(
schedule模板关键字),配合计划金额调整(percent/fixed)与沉没分摊逻辑,让「计划驱动的预算」真正自动化;同时打磨了侧边栏浮动交互,修复了撤销快捷键与零值对账两个影响日常使用的缺陷,并清理了旧的自动补全依赖; - 服务端:补齐了
reset-password运维能力(npm 脚本 +--reset-password参数双入口),基于 Argon2id 哈希并保持 bcrypt 向后兼容,还内置了对 OpenID-only 实例的保护;Nordigen 异常报告也得到增强。
对于自托管用户,升级到23.4.2后最值得立刻上手的功能就是npm run reset-password——从此忘记密码不再需要动数据库。对于开发者,schedule模板的实现(schedule-template.ts)与密码管理实现(password.js)都是理解 Actual 预算引擎与认证体系的上佳入口,配套的 schedule-template.test.ts 与 password.test.js 则直接给出了各种边界行为的行为契约。
【免费下载链接】actualA local-first personal finance app项目地址: https://gitcode.com/GitHub_Trending/ac/actual
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考