Haruka 取自《明日方舟》遥干员的英文名。
作者曾经是随手记的拥簇,但是随手记太大太冗余太臃肿了,以及最近正好在找记账app,没有一个符合自己要求的记账App,所以就有了轮子再创造 —— Haruka
Haruka 是一个单例软件,理论上它只服务于一个用户。
需要 Rust stable、Node.js 24 和 npm。Ubuntu/Debian 从源码构建还需要 C 编译工具及 OpenSSL 开发文件:
sudo apt-get update
sudo apt-get install build-essential pkg-config libssl-dev首次运行先安装前端构建依赖并生成本地浏览器资源:
npm ci
npm run web:build
cargo runTailwind 样式由 assets/tailwind.css 扫描模板和 Rust 源码后编译到 static/app.css,开发样式时可使用 npm run css:watch。static/app.css 是被 Git 忽略的本地构建产物,不提交;本地编译 Rust 前须先生成它,CI 和 Docker 也会先运行 npm run web:build 再编译 Rust。web:build 还会复制锁定版本的 htmx、Chart.js、Tesseract.js Web Worker/WASM 和简体中文、英文 OCR 模型;这些浏览器资源仍提交在 static/,CI 单独检查它们与锁定依赖一致。所有资源均嵌入二进制,运行时不依赖第三方 CDN。
浏览器会注册一个轻量 Service Worker,仅缓存编译后的 CSS 和锁定版本的非敏感前端运行文件。包含账户、账单等解密内容的 HTML/JSON 不会进入离线缓存,任何 AJAX 或账务写入也不会离线排队;断网操作会明确失败,恢复网络后不会自动重放。
首次设置密码、解锁和密码恢复页面不渲染顶部菜单栏;解锁页内的 Passkey 兼容绑定也保持无菜单布局。成功解锁进入仪表板后显示正常导航,主动锁定后重新隐藏菜单。菜单在模板层移除,不依赖 JavaScript 或 CSS 隐藏。
快速记账的“扫描票据”在浏览器 Web Worker 中运行 Tesseract.js WASM,图片不会上传至 haruka 后端。首次识别需要从 haruka 自身加载约 18 MB 的本地 OCR 运行文件和中英文模型,之后语言数据由 Tesseract.js 缓存在浏览器 IndexedDB 中。
识别后的文字可以用本地规则提取金额和时间,也可以由浏览器直接调用用户填写的 OpenAI Chat Completions 兼容完整 URL。URL 与模型只保存在当前浏览器 localStorage,API Key 不持久保存、刷新页面即清除;后端不会代理或看到 URL、密钥、图片及 OCR 文字。自定义 AI 服务必须允许 haruka 当前来源进行 CORS 请求,否则浏览器会明确报告跨域失败。所有识别结果都只填入可编辑快速记账草稿,仍需用户确认后才会写入账单。
订阅可以绑定一个同币种账户,并设置在到期前 0 到 30 天开始检查余额。到期后,已解锁浏览器访问仪表板时会调用受会话保护的接口生成普通支出账单,并把订阅顺延一个周期;这只是在 Haruka 账本中自动记账,不会连接银行、信用卡或支付平台发起真实扣款。服务锁定或重启期间无法使用 DEK,遗漏的到期订阅会在下次解锁进入仪表板后补执行。
普通账户按余额判断是否足够,信用卡和信贷服务按剩余可用额度判断。进入提前检查期后若不足,仪表板顶部会显示高优先级红色提醒;余额不足时不会生成账单或顺延日期。信贷服务还会在仪表板独立展示当前欠款、已用额度、剩余额度、总授信额和额度使用比例。
“预算”页面可以分别设置日预算、周预算和月预算,留空即可单独停用某个周期。预算金额使用默认货币并加密保存;普通支出会按汇率折算后计入自然日、周一至周日和自然月的当前进度,转账、借还和定投本金不占用预算。预算页和仪表板都会显示已使用比例、剩余金额或超支金额,达到 80% 后用醒目样式提醒。
顶部“分类”入口打开独立的 /categories 页面,收入和支出分别管理;设置页只保留跳转入口。支出分类可以选择每日、每周或每月的笔数限额,默认自然月,填写正整数启用,留空停用;收入分类没有笔数限额。限额数值加密保存。
按访问者时区统计当前自然周期截至今天的普通支出账单,每笔计一次,未来日期暂不计入,不受账户或币种影响。订阅、短信生成的支出及定投/分期手续费同样计入;收入、转账、借还和定投本金不计入。达到 80% 或仅剩一笔时,分类页及仪表板醒目提醒;达到、超出分别标记。限额是消费提醒,不阻止真实支出入账。
账单保存稳定的分类关联,并保留原来的分类名称与食品标记快照。升级后的分类改名不会重置次数,删除后同名重建不会自动继承旧账单;编辑账单重新选择当前有效分类时,计数会随关联更新。
启动时自动补齐旧数据库字段,无需删除数据库。首次解锁只为尚未关联的旧账单按当前同类型、同名分类建立关联,无法匹配的记录不会自动计入后来创建的分类。升级前的数据没有稳定分类身份,因此无法复原此前改名、删除及同名重建的关联历史;旧账单的金额、名称、食品标记和时间不被改写。
业务时间始终按 UTC 保存,不迁移或偏移旧数据。启用 JavaScript 时,浏览器通过非敏感的 haruka_time_zone 会话 Cookie 提供 IANA 时区;日期搜索、账户本月汇总、仪表板、统计和日/周/月预算使用同一当地自然日口径,并在页面显示当前时区。没有时区时明确回退到 UTC。账单搜索可用 time_zone 指定口径,筛选、翻页和 CSV 导出均保留该值;纯日期还款日不转换,定投交易日仍按北京时间判断。
快速记账在 JavaScript 不可用时仍显示收支、转账和借还表单,时间明确按 UTC 输入;提交保存用户填写的时间,并按 redirect_to 返回仪表板或独立记账页。
统计页包含收入与支出的分类、账户排行和占比饼图;分期撤销会同时校验本金扣回账户及返还本金、利息、手续费账户的最终余额,任何一端不满足余额约束时整次撤销不写库。
仪表板和统计页共用在公共页面头部预先加载的本地 Chart.js,确保 htmx 点击导航时图表库已就绪;同一文档内切页不重复加载图表库。离开图表页时销毁旧实例,首次进入、重复切页和浏览器前进/后退均可正常绘图,无需手动刷新。
账单页和高级搜索页均可导出当前筛选结果。CSV 包含普通收支、转账、借还及余额调整的合并流水,不受当前分页限制;日期筛选和导出时间使用页面显示的同一时区。文件使用带 BOM 的 UTF-8 编码以兼容常见表格软件,并对账户、分类、对象及备注等用户文本进行电子表格公式注入防护。
普通收入或支出账单可以创建限时分享链接,地址格式为 /s/<随机字符>。创建时可设置精确的结束时间和可选访问密码;分享管理页可以重新复制链接或随时撤销。公开页面只展示创建当时的账单快照,账户卡号和用户名仍保持掩码,之后编辑或删除原账单不会修改已经发出的内容。
快照使用分享 URL 中的随机令牌与可选密码共同派生的独立密钥加密,密码不会保存到数据库。分享访问不依赖已解锁会话,因此服务重启后链接仍然可用;超过有效期或被撤销后立即停止展示。公开响应禁止缓存和搜索引擎收录。生产部署请使用 HTTPS,并注意 URL 本身就是访问凭据:只交给需要查看的人,设置密码时最好通过另一渠道发送;反向代理也应避免长期记录完整的 /s/ 请求路径。
“借还请求”可以生成一个限时加密链接,把期望金额、收款人户名和收款账户分享给打款人。完整卡号或账户用户名不会直接写进公开页面 HTML;打款人点击复制时,浏览器才通过禁止缓存的接口读取并写入剪贴板。
打款人可以填写姓名、联系方式、打款账户、实际金额、时间和说明。这些内容使用分享 URL 中的 256 位随机令牌派生独立密钥后加密保存,URL 令牌本身再使用账本 DEK 加密,数据库不会保存可直接读取的个人信息。公开提交只是待确认声明,不会直接修改余额;账本所有者核实到账并确认后,系统才创建“借入”或“收回还款”流水。未指定既有借贷对象的借款请求会在确认时根据打款人信息创建对象。
请求创建、打款人提交和到账确认依次组成 SHA-256 验证链,公开回执和管理页会显示当前链头指纹。双方保存并核对指纹可以发现事后改写;这是一种单机防篡改凭证,不宣称具有多节点区块链的去中心化共识能力。
顶部“理财”(/finance)参考《我怎么开始多多理财》提供可编辑的起点:入门模板按月收入的 10% 投资,纳斯达克 100 / 沪港深 500 各占一半;均衡参考按 30% 投资,海外股票、国内股票、债券、黄金各占 25%。也可以完全自定义资产名称、类别、投资比例、应急金月份、扣款账户、具体基金、估算交易日、固定或聪明定投策略及手续费。
- 收入推算:默认按访问者时区统计最近 3 个完整自然月的普通收入,范围可设为 1–12 个月;包含零收入月,排除尚未结束的本月,转账与借还不计入。多币种收入按当前最近可用参考汇率折算,均值四舍五入到分。没有正收入或无法取得必要汇率时明确要求手动填写;手动月生活费 / 收入基数始终可以覆盖推算,一次性进账需自行核对。
- 预算与应急金:月投资预算为采用基数乘自定比例;其余为生活费 / 现金留存参考。应急金目标按完整基数乘目标月份保守估算,可设为 0 停用,也可选择多个非信用账户查看当前应急金、缺口及覆盖月份。应急不足只提醒,不替用户作投资决定。方案货币保存在配置中,修改系统默认货币不会静默改变既有方案。
- 资产配置与预览:最多 20 项,各比例支持两位小数,必须精确合计 100.00%,同一具体基金不能重复归类。月预算按整数分做最大余数分配,每日基准再按估算交易日向下取整到分;默认 20 天不是实际月交易日保证。异币种扣款先按明确显示日期的参考汇率换算,再生成扣款账户原币的每日金额,不使用 1:1 假定。聪明定投会在基准的 50%–150% 间调整,手续费另计,因此月预算不是扣款硬上限。
- 明确确认后才生成:保存只加密保存配置并刷新服务器预览,不修改账户余额。确认才会在一个事务中创建必要的零价值投资分类、具体基金和实际定投,或更新本方案已跟踪的计划。重复同步复用原计划,不重复创建;零预算或零配比暂停但保留映射,恢复时不补扣暂停期间日期。移除项目暂停旧计划、不删除历史,不影响用户另建的计划。已有启用计划保留原开始日期和执行进度,表单起始日只作用于新增计划。
- 防止旧预览误操作:并发保存、收入/汇率或已跟踪计划变化导致版本或预览不一致时拒绝同步,需重新查看预览再确认;任何基金或计划创建失败都会整批回滚。配置及计划关联以 DEK 加密保存,页面和 JSON 禁止缓存,htmx 不保存理财页的本地历史快照。
- 失效账户可修正:已选扣款账户、基金或应急金账户被删除或类型发生变化时,理财编辑器仍可进入,明确提示重新选择并禁止生成。原配置不会被自动清空;重新保存和生成时仍严格校验账户,读取失败或数据库错误不会被当成正常配置。
- 定期复核:预览按当前已绑定基金价值显示目标金额和再平衡差额,只供参考,不会自动校准资产或转账。请先在基金校准页面核对真实持仓,再调整配置;方案不会连接银行或购买金融产品,也不保证收益。生成后的实际执行、余额校验、交易日历及补执行仍使用下述现有定投系统。
“定投”页面可创建每日基金定投计划。计划固定绑定一个非信用扣款账户和一个投资分类下的具体基金,两端必须使用相同货币,并可设置手续费率,例如 0.15%。每个中国大陆交易日会把本金以普通转账转入该基金;实际手续费按本期本金乘费率计算并四舍五入到分,额外从扣款账户扣除,单独生成“投资手续费”支出。这样本金不会被误算成消费,而手续费会正常进入支出统计。
“投资账户”现在是基金分组,例如建立“沪港深 500”分类,再添加不同名称的基金。每只基金独立记录资金流水和持仓价值,分类总价值自动汇总;转账、记账和定投必须选择具体基金,不能直接向分类总额入账。基金继承分类货币,分类建立后不能直接修改类型或货币。有持仓、历史流水或关联配置的基金不能删除,以保留历史。
升级后首次解锁会自动将每个旧投资账户迁移为“原投资账户 → 默认基金”。原持仓价值、余额调整、资金流水和计划关联保留,旧计划的银行短信基金名称移入默认基金的加密别名,不需要删除数据库。招商银行及自定义定投短信按扣款银行卡尾号和具体基金的名称或银行别名匹配,不按投资分类名称匹配;银行别名在基金编辑页设置。多个计划同时匹配时拒绝自动选择。
扣款策略可选择固定金额、“聪明定投”、每日手动金额或银行短信实扣。手动模式会在每个到期交易日要求用户填写当天实际本金;短信模式则按内置招商银行模板或用户配置的成功模板中的实际扣款金额生成账户转账,失败短信只形成提醒和审计记录。短信实扣计划目前只支持 CNY 账户。所有模式都以“计划 + 日期”去重,同一天不会重复扣款。
短信接口为 POST /api/sms,请求体为 {"time":"2026-09-15T10:00:00+08:00","raw":"【招商银行】...","sender":"95555"},其中 time 必须是带时区的 ISO8601 日期时间,sender 可省略且默认为空字符串。请求头推荐使用标准的 Authorization: Bearer <token>,也兼容现有调用方的 Authentication 头。可以在设置页输入或随机生成专属 Token,数据库只保存不可逆的 SHA-256 校验值;没有页面专属 Token 时使用 SMS_API_TOKEN,两者均未配置时兼容使用 AppleToken。接口先匹配启用的自定义模板,没有匹配时回退原有招商银行定投成功、定投失败、实时转至他行和快捷支付模板,旧快捷指令无需新增模板。内置模板核对短信年月日及时分与 time 的北京时间;定投还同时核对基金名称和扣款银行卡尾号。
设置页可加密保存本人姓名。使用内置招商银行模板时,实时转账的收款人与本人姓名完全一致会进入“本人账户转账”待确认状态;包含“信用卡还款-本人姓名”的快捷支付会进入“信用卡还款”待确认状态。由于这两类短信不包含目标账户卡号,Haruka 不会擅自修改余额,必须在独立“短信”页(/sms)选择本人名下的目标账户后才能生成转账。其他收款人或普通快捷支付只保留为无法确认的加密短信记录。
短信页支持关键词、状态筛选和 50/100/200 条分页,旧待确认候选不会因为新短信而失去入口。解锁后的补处理使用 POST /sms/process-pending,确认使用 POST /sms/{id}/confirm;定投页只保留计划和短信页入口。补处理前数据库查询失败不会消费内存队列,恢复后可以重试。
聪明定投支持沪深 300(000300)、中证沪港深 500(H30455)和恒生指数(HSI),均线周期可选 120、180、250 或 500 个指数交易日,默认 180 日。执行 T 日计划时,使用 T 日之前最近一个指数交易日的收盘价(即正常情况下的 T-1)和截至该日的均线比较:偏离不超过 2% 时仍按基准金额的 100%;高于均线 (2,3]、(3,4]、(4,5]、(5,6]、(6,+∞) 时依次按 90%、80%、70%、60%、50%;低于均线的相同区间依次按 110%、120%、130%、140%、150%。页面可在保存前联网预览当前档位。
这里的“执行”只会在 Haruka 内生成资金流水,不会连接招商银行、基金销售平台或自动发起真实扣款和申购。
沪深 300 和中证沪港深 500 日线来自中证指数官网,恒生指数日线来自东方财富公开行情;行情会持久缓存。联网更新失败但本地已有足够的历史数据时会明确提示并继续使用缓存,缓存不足时保留待执行计划并报错,不会静默按固定金额执行。每次成功执行都会固化基准金额、扣款比例、行情日期、收盘点位和均线,之后行情数据变化也不会改写历史计算依据。手续费按聪明定投调整后的本期本金计算。
haruka 按北京时间判断交易日:周六、周日不执行,并排除上海证券交易所公告的休市日。程序内置 2025、2026 年官方休市安排;后续年份可在定投页面的“交易日历校准”中补充工作日休市日期。
由于数据库 DEK 只存在于已解锁的内存会话,服务锁定或重启后不能在后台无人值守地解密定投金额。期间遗漏的交易日会保留,用户解锁并访问定投页后由联网 AJAX 幂等补执行;同一计划同一交易日最多生成一次流水。余额不足时计划会保持待执行并显示原因,不会静默跳过。短信到达时若已有任一解锁会话会立即处理;否则原文只暂存在服务内存,待下次解锁后处理,不会明文落盘,但服务重启会丢失尚未处理的内存短信。处理后的原文和结果使用账本 DEK 加密保存。
基金不维护代码、份额或每日净值;聪明定投读取的指数行情只用于计算本期扣款比例,不代表基金净值或真实成交价。投资分类的“批量校准基金价值”页面左侧显示每只基金当前价值,右侧预填可编辑的实际价值。留空或未修改的行不写入;明确填写 0 会清空该基金价值。价值未变但已核对时可勾选“已核对”,只更新核对日期并保留零差额审计。整批校准在同一事务中提交;任何已修改或已核对的基金出现新流水或估值冲突,整批拒绝保存。校准差额只产生不可删除的余额调整审计,不生成转账,也不计入普通收入、支出或预算。
建议每月从基金平台核对各基金实际持仓价值。正持仓满 30 天未校准或核对时,仪表板会提示,并可从顶部“基金校准”(/funds/valuation-reminders)进入;空持仓不提醒。分类总价值随各基金价值变化自动汇总,不能独立覆盖。
进入“短信 → 短信模板”(/sms/templates),可以新增、编辑、启停和删除模板。页面提供招商银行定投成功/失败、实时转账、快捷支付以及通用金额/内容预设;点击预设只填入可编辑草稿和样例,不会保存或记账。
每个模板配置以下内容:
- 发送号码、银行标记:至少设置一个,例如
95500和示例银行。银行标记匹配短信中【】内的完整内容;两项同时填写时必须全部符合。配置号码条件后,短信转发工具必须在sender中传入实际发送号码。号码和银行标记只是匹配条件,不能代替 API 令牌认证。 - 命名正则:用
(?P<amount>...)提取金额、(?P<content>...)提取备注内容。定投可以额外提取fund、last4,也可直接绑定一个启用的短信实扣计划;未绑定计划时必须提取基金名和卡尾号,绑定后提取到的基金名或卡尾号仍须校验。如果提取year/month/day/hour/minute,会与time的北京时间核对。 - 匹配后动作:默认仅保存审计,不改变余额。可选定投成功/失败、转出候选、转入候选,或明确启用自动生成普通支出/收入账单。普通账单必须选择账户及对应类型分类;金额使用所选账户原币,执行前仍校验余额、授信额和信贷服务余额上限。
例如,短信 【示例银行】支出12.34元,内容:午餐。 可以使用:
^【示例银行】支出(?P<amount>[0-9]+(?:\.[0-9]{1,2})?)元,内容:(?P<content>.+?)[。.]?$先填写样例时间、号码与原文,点击“预览匹配结果(不保存)”查看金额和内容,确认后再保存启用。预览不接收短信、不保存样例、不创建审计或资金流水,也不会校验其他已保存模板是否同时匹配。表达式使用 Rust regex,不支持环视和反向引用,长度最多 4096 字节并限制编译复杂度;金额必须是正数且最多两位小数。
自定义模板优先匹配,但多个启用模板同时匹配同一短信时会明确报错,不会按顺序随意选择或记账。可以停用或收紧重叠模板,再在短信页补处理;余额不足或配置引用失效的事件也保留供修复后重试,已成功事件不会再次写入账单。
转入/转出模板只生成候选:模板绑定自己的来源/目标账户,用户须在短信页选择另一同币种账户后确认;确认会按实际方向校验两端余额,不允许用信用额度转出。候选保存独立的加密匹配快照,因此事后编辑或删除模板不改变已识别的金额、方向和账户绑定。短信入账仅记入 Haruka 账本,不向银行发起支付。
所有待确认候选都可选择“确认并记账”或“忽略,不记账”。忽略不会生成账单或转账,也不改变余额;忽略后默认列表隐藏该记录,可筛选“已忽略”查看保留的加密审计。重复接收同一短信或点击补处理不会将其恢复为待确认;已记账的记录不能通过忽略撤销。确认与忽略共用写入锁,同一候选不能同时完成两种操作。
模板名称、号码、银行标记、表达式以及短信原文、号码和解析快照均使用账本 DEK 加密保存。锁定时短信只能暂存在服务内存;补处理读取数据库失败不会消费内存队列,逐条失败也会保留供重试。现有数据库会在启动时自动补齐字段,无需删除数据库。
仓库内有两条构建工作流:
.github/workflows/build.yml会在 push、Pull Request 或手动触发时构建 Linux 二进制;.github/workflows/container.yml会构建linux/amd64和linux/arm64镜像。Pull Request 只验证镜像能够构建,推送到main或推送v*标签时才发布到 GHCR。
二进制工作流会执行以下检查:
- 使用
npm ci安装锁定的前端依赖并重新生成浏览器资源; - 检查仍跟踪的
static/浏览器运行库、OCR 模型和扫描脚本是否与锁定依赖一致;生成的static/app.css不提交,也不纳入资源一致性检查; - 执行
cargo fmt --check和cargo build --locked --release; - 上传保留 14 天的
haruka-linux-x86_64构建产物。
二进制工作流使用 Swatinem/rust-cache 缓存 Cargo 依赖和 target/ 编译产物(包含本项目),而不只是下载的依赖源码。缓存随 Rust 编译器、依赖和构建环境变化而失效;恢复缓存后仍正常执行格式检查和 release 构建,由 Cargo 判断哪些文件需要重新编译。
从 GitHub Actions 页面下载并解压 haruka-linux-x86_64.tar.gz 后即可取得在 Ubuntu 24.04 x86_64 上构建的可执行文件。CSS、前端运行库和 OCR 模型均已嵌入二进制,部署机器不需要安装 Node.js,也不需要额外复制 templates/ 或 static/。其他操作系统或较旧的 Linux 发行版建议按照“本地运行”一节从源码构建。
容器工作流使用仓库自带的 GITHUB_TOKEN 发布 ghcr.io/<仓库所有者>/haruka,不需要额外配置镜像仓库密码。main 对应 latest 和 main 标签,版本标签(例如 v0.2.0)会发布 0.2.0、0.2 等标签,同时每次构建还会生成提交 SHA 标签。首次发布后可在 GitHub Packages 设置中将镜像改为 Public;如果保持 Private,部署机器需要先使用具有 read:packages 权限的令牌执行 docker login ghcr.io。
容器工作流通过矩阵在 ubuntu-24.04 和 ubuntu-24.04-arm 上分别原生构建 AMD64、ARM64,不再使用 QEMU 模拟编译。每个架构先用 cargo-chef 构建 Rust 依赖,并将包含编译产物的镜像层独立导出到 haruka-rust-deps-amd64 / haruka-rust-deps-arm64 缓存;生产镜像同时读取该架构的依赖缓存和 haruka-container-amd64 / haruka-container-arm64 镜像缓存,避免不同架构互相覆盖。日常源码、模板和静态资源修改可复用依赖层,应用仍会重新编译;基础镜像固定到 digest,升级基础镜像或修改依赖时会重建依赖层。依赖步骤成功导出后,即使后续镜像构建失败或取消,该架构的依赖缓存仍能供下一次使用。首次切换到新缓存作用域需要冷构建。
两种架构各自有 60 分钟的 job 上限,并行构建时互不占用对方的时间;一个架构失败不会提前取消另一个架构的缓存构建,但会阻止正式标签发布。非 PR 构建先按 digest 推送各架构镜像,保留 provenance 和 SBOM;仅在两者全部成功后,由独立的 10 分钟合并 job 发布包含双架构的 latest、分支、版本及 SHA 标签。合并 job 只下载 digests-* 产物,避免误读 Docker 构建记录。PR 使用显式 cacheonly 输出验证两个架构,不登录或推送 GHCR,不上传发布用 digest 或运行合并 job。仍保留构建检查和警告输出;冷构建耗时及缓存可用性取决于 runner 和网络,不能保证固定完成时间。
镜像默认监听容器内的 0.0.0.0:3000,默认把 SQLite 数据库放在 /data/haruka.db。下面使用 Docker 命名卷保存整个数据库目录,并只把服务发布到宿主机回环地址,适合在 Caddy、Nginx 等 HTTPS 反向代理后运行:
如需先在本机从当前源码构建镜像,可执行 docker build --tag haruka:local .;多阶段 Dockerfile 会在构建阶段重新生成 Tailwind CSS 和 release 二进制,最终镜像不包含 Node.js、Rust 工具链或源码。
export HARUKA_IMAGE='ghcr.io/YOUR_GITHUB_OWNER/haruka:latest'
docker volume create haruka-data
docker pull "$HARUKA_IMAGE"
docker run -d \
--name haruka \
--restart unless-stopped \
--publish 127.0.0.1:3000:3000 \
--mount type=volume,src=haruka-data,dst=/data \
--env PORT=3000 \
--env PASSKEY_ORIGIN='https://haruka.example.com' \
--env PASSKEY_RP_ID='haruka.example.com' \
"$HARUKA_IMAGE"将 YOUR_GITHUB_OWNER 替换为发布镜像的 GitHub 用户名或组织名,并把 Passkey 两项改成实际对外域名。升级时保留同一个命名卷并重建容器:
docker pull "$HARUKA_IMAGE"
docker rm --force haruka
docker run -d \
--name haruka \
--restart unless-stopped \
--publish 127.0.0.1:3000:3000 \
--mount type=volume,src=haruka-data,dst=/data \
--env PORT=3000 \
--env PASSKEY_ORIGIN='https://haruka.example.com' \
--env PASSKEY_RP_ID='haruka.example.com' \
"$HARUKA_IMAGE"也可以使用 Compose:
services:
haruka:
image: ghcr.io/YOUR_GITHUB_OWNER/haruka:latest
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
environment:
PORT: "3000"
PASSKEY_ORIGIN: https://haruka.example.com
PASSKEY_RP_ID: haruka.example.com
volumes:
- haruka-data:/data
volumes:
haruka-data:保存为 compose.yaml 后执行 docker compose up -d。镜像已经设置 DATABASE_URL=sqlite:///data/haruka.db?mode=rwc;如需改用其他容器内目录或文件名,可在 environment 中覆盖。备份时应备份整个 haruka-data 卷,因为 SQLite 运行时可能同时存在 WAL 和 SHM 文件。
如果要直接向局域网发布而不使用同机反向代理,可把端口映射改成 3000:3000。Passkey 在非 localhost 环境仍然需要 HTTPS,不能仅靠开放 HTTP 端口使用。
不传配置时 haruka 只监听 127.0.0.1:3000。配置优先级为:命令行参数、LISTEN_ADDR、PORT、默认值。
| 配置 | 行为 |
|---|---|
--listen 127.0.0.1:8080 |
精确设置监听地址和端口,适合放在同机反向代理后面 |
--port 8080 |
监听 0.0.0.0:8080 |
LISTEN_ADDR=0.0.0.0:8080 |
通过环境变量精确设置监听地址和端口 |
PORT=8080 |
监听 0.0.0.0:8080,适合自动注入 PORT 的部署平台 |
SMS_API_TOKEN=随机长令牌 |
设置 /api/sms 的 Bearer Token;未配置时为兼容当前调用方使用 AppleToken |
直接运行二进制时:
./haruka --listen 127.0.0.1:8080
./haruka --port 8080
PORT=8080 ./haruka从源码运行时,需要用 -- 把参数传给 haruka:
cargo run -- --port 8080--port 和 PORT 会监听所有网络接口。若不需要外部设备直接访问,建议使用 --listen 127.0.0.1:端口,再通过 HTTPS 反向代理提供服务。
默认数据库是当前工作目录下的 haruka.db。生产环境建议把数据库放在持久化目录,并确保运行用户拥有该目录的写权限:
DATABASE_URL='sqlite:///var/lib/haruka/haruka.db?mode=rwc' \
./haruka --listen 127.0.0.1:3000持久化或备份时需要保留 haruka.db,以及 SQLite 运行期间可能出现的 haruka.db-wal、haruka.db-shm。容器部署时应把整个数据库目录挂载到持久卷,而不是只挂载单个文件。
服务器还需要能够通过 HTTPS 访问 api.frankfurter.dev 获取汇率。网络不可用时会回退到数据库中的历史汇率缓存;某个货币对完全没有缓存时,相关操作会向客户端明确报错。
生产环境的 Passkey 必须使用 HTTPS,并且公开来源和 RP ID 在首次注册后应保持不变:
DATABASE_URL='sqlite:///var/lib/haruka/haruka.db?mode=rwc' \
PASSKEY_ORIGIN='https://haruka.example.com' \
PASSKEY_RP_ID='haruka.example.com' \
./haruka --listen 127.0.0.1:3000例如使用 Caddy 终止 TLS:
haruka.example.com {
reverse_proxy 127.0.0.1:3000
}PASSKEY_ORIGIN 必须是用户在浏览器中实际访问的完整 HTTPS 来源,PASSKEY_RP_ID 只填写域名。切换域名、协议或 RP ID 后,原有 Passkey 将无法继续使用。
假设二进制位于 /opt/haruka/haruka,数据库目录为 /var/lib/haruka:
[Unit]
Description=haruka accounting service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=haruka
Group=haruka
WorkingDirectory=/var/lib/haruka
ExecStart=/opt/haruka/haruka --listen 127.0.0.1:3000
Environment="DATABASE_URL=sqlite:///var/lib/haruka/haruka.db?mode=rwc"
Environment="PASSKEY_ORIGIN=https://haruka.example.com"
Environment="PASSKEY_RP_ID=haruka.example.com"
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target修改后执行 systemctl daemon-reload,再使用 systemctl enable --now haruka 启动服务。
本地默认使用当前监听端口对应的 http://localhost:<端口> 作为 WebAuthn 来源;需要用这个地址访问(不要改用 127.0.0.1)才能注册和登录 Passkey。部署到其他域名时固定配置:
PASSKEY_ORIGIN=https://haruka.example.com PASSKEY_RP_ID=haruka.example.com cargo run生产环境必须使用 HTTPS。已有 Passkey 与来源和 RP ID 绑定,后续不要随意更改这两个值。
本机登录入口使用可发现凭据流程,包含通过 iCloud 钥匙串同步到当前设备的 Passkey;手机和 Mac 必须访问注册时相同的 HTTPS 域名与 RP ID,不能把 Mac 的 localhost 换成局域网 IP。同步了凭据不等于当前系统或凭据提供方支持用于解密的 PRF;没有 32 字节 PRF 输出时不能降级为仅凭签名解锁。
注册追加确认保持原认证器模式,并只允许选择刚创建的凭据。若 WebAuthn 签名已验证,但本次 PRF 无法解开已有 DEK 包裹,登录页允许在五分钟内用主密码做一次设备兼容绑定;新包裹追加保存,原 Mac 或其他设备的包裹不被覆盖。绑定过期可重新点击 Passkey 进行认证,不需要删除原凭据。外部登录按钮目前保持隐藏,设置页仍可选择外部认证器注册。
Passkey 注册与兼容绑定表单禁用 htmx boost,由页面 JavaScript 提交,避免同时发起刷新请求而中断注册或清空绑定提示。主密码错误会保留绑定表单供重试;绑定成功后通过完整导航进入仪表板。
服务端备份状态变化、相同 PRF 登录及不同 PRF 的主密码绑定路径已用软件认证器签名验证;这不替代真实 iPhone/iCloud 的兼容性验证。若同步后无法登录,请记录页面错误原文,并核对两端访问域名与系统的 PRF 支持。
- 内置订阅管理,妈妈再也不用担心我忘了续费啦!
- 恩格尔系数看板(闲着没事写上去的哈哈哈)
- 自动化 OCR 设计,你只需要确认
- 可选的AI Endpoint(AI传输内容不过服务端,你怎么设置怎么来,你完全可以使用本地的 ollama 来进行回复!)
- 自带 iCloud Shortcuts,你甚至可以直接截图然后自动记账(截图目前预计支持微信/支付宝/四大加一招行)
- 货币支持(CNY/HKD/USD,汇率随时变动)
- 可能的ETF,持仓等分析
本项目使用 vibe coding 技术强力驱动并使用 MIT 授权协议,你想怎么用就怎么用去。