我是那种脑子停不下来的人。工作时、阅读时、睡前那几分钟,脑子里总会冒出各种念头:一个好的产品想法、一句值得记住的话、一个对工作问题的解法。但这些想法来得快,忘得也快。有时掏出手机打开备忘录,打完字却发现索然无味——那段口语化的碎碎念躺在备忘录里,和几百条同样潦草的记录挤在一起,永远不会有重见天日的时候。
我一直想要这样一个东西:只管打字,什么都不用管,第二天早上能看到一份整洁的整理报告。不是那种需要手动打标签、选分类的“重工具”,而是真正零摩擦的记录——就像对着一个永远不会烦你的朋友说话,他听完点点头,回去默默帮你整理好,第二天把一份漂亮的笔记放在你桌上。
这个东西不存在,所以我做了一个。
一、整体思路:手机打字,NAS 存数据,AI 做整理
项目的核心流程很简单:Android 手机上打字保存 → 通过 WebDAV 协议存到家里的群晖 NAS → 每天凌晨 DeepSeek AI 自动对原始记录进行分类和润色 → 生成日、周、月、季、年五种 Markdown 总结报告。整个过程不需要任何手动操作,AI 全自动运行。
架构上分为两块:前端是一个极简的Android App,用 Kotlin 和 Jetpack Compose 写,只有四个页面——首页输入、历史浏览、日期详情、NAS 设置。后端是一套 Python 脚本,跑在群晖 NAS 上,通过 DSM 任务计划定时触发。AI 引擎用的是 DeepSeek 的 API,兼容 OpenAI 格式,调用成本极低。
整个数据流向是:用户输入口语化文字→ App 通过 WebDAV 写入 NAS 的 raw 目录 → 凌晨 daily_summary.py 读取原始文件,调用 DeepSeek 分类润色 → 输出到 daily 目录 → 周报/月报/季报/年报依次从下层报告中汇总生成。每一层都是纯 Markdown 文件,人可以随时打开看,AI 也能读懂上下文。
二、技术选型:务实优先,不追新
技术栈的选择上,我坚持一个原则:够用就好,不过度设计。
(一)Android 端:Kotlin + Compose + Hilt
Android 端选择了 Kotlin 2.0 + Jetpack Compose + Material 3 的组合。Compose 写 UI 的效率比传统 XML 高得多,Material 3 默认支持 Android 12 以上的动态取色(Material You),省去了大量主题定制时间。依赖注入用的是 Hilt 2.51,虽然配置稍繁琐,但编译期 DI 的稳定性在个人项目中很省心。本地缓存用了 Room 2.6,只缓存最近 7 天的数据,保持轻量。网络层选了 OkHttp 4.12,直接实现了 WebDAV 的 PUT/GET/PROPFIND/MKCOL/DELETE 等 HTTP 方法,没有引入第三方 WebDAV 库——因为 WebDAV 本质就是 HTTP 协议的扩展,用 OkHttp 手写几百行就能覆盖所有需要的方法。
(二)NAS 端:Python 3 + DeepSeek API
NAS 端用 Python 3 编写,没有用任何 Web 框架——就是一套命令行脚本,由 DSM 的任务计划直接调用 python3 执行。选择 Python 是因为它处理文本和调用 API 的便利性,加上群晖系统自带 Python 3.8 环境,不需要额外安装运行时。
AI 引擎选了 DeepSeek,而不是 OpenAI。理由很简单:便宜。DeepSeek Chat 模型的处理能力对于文本分类和润色这个场景完全够用,每千 token 的价格是 GPT-4 的几十分之一。实测下来,每天处理 10 条记录的 API 费用大约是 0.002 美元,月费不超过 7 美分,四舍五入等于不要钱。技术上用的是 OpenAI 兼容接口,一行代码都不用改,只需要把 base_url 从 openai.com 改成 api.deepseek.com。
(三)通信协议:WebDAV 而非自建 API
App 和 NAS 之间的通信没有自建 API 服务,而是直接用群晖自带的 WebDAV Server。做这个决定是因为:第一,我不想在 NAS 上多维护一个常驻的 Web 服务进程;第二,WebDAV 是 HTTP 协议的标准扩展,OkHttp 原生支持需要的所有方法;第三,WebDAV 天然支持目录结构、文件锁、认证——这些恰好是这个项目所需的核心能力。代价是文件写入需要实现完整的锁机制(读-改-写-解锁),多写了几十行代码,但换来的是零运维负担。
三、开发中踩过的七个坑
如果我说开发过程一帆风顺,那一定是在说假话。以下是真实的踩坑记录,按时间顺序排列。
(一)Gradle 版本的连环错
用Android Studio 首次打开项目时,AS 自动把 Gradle 从 8.9 升级到了 9.4,又把 AGP(Android Gradle Plugin)从 8.5.2 悄悄改成了 7.4.2,还顺手把 Kotlin 从 2.0 降回了 1.8。结果是连 Sync 都过不了,报了一整套版本冲突错误。最终的做法是逐一手动恢复 gradle-wrapper.properties、libs.versions.toml、settings.gradle.kts 和根 build.gradle.kts 四个文件。教训是:AS 的自动迁移功能在版本跨度大的时候并不可靠,版本目录(Version Catalog)的项目最好先把这些文件提交到 Git,出问题了直接 git checkout。
(二)kotlin.Result 的语法陷阱
Kotlin 标准库的 Result 是一个 inline value class,不是 sealed class。这意味着你不能用 is Result.Success 或 is Result.Failure 来做 when 分支判断——编译会直接报错。正确的用法是 result.fold(onSuccess = {...}, onFailure = {...}) 或 result.isSuccess/result.isFailure。我在 EntryRepository 和 WebdavApi 里各写了一处这个错误,都是因为手快写成了 when 模式。这个错误很容易犯,因为它看起来太像 sealed class 了。
(三)Hilt 依赖注入的 MissingBinding
Hilt 编译时报了 EntryDao cannot be provided without an @Provides-annotated method。问题在于我在 AppModule 里只提供了 AppDatabase,但没有单独提供 EntryDao。Room 的 @Dao 注解不会自动让 Hilt 识别——需要在 Module 里显式写 provideEntryDao(db: AppDatabase) = db.entryDao()。这是我第一次在 Hilt 里搭配 Room 使用,少了这一步导致 KSP 处理器报了一长串错误,排查了好一阵。
(四)WebDAV 路径多一层 /homes
App 的 WebDAV 地址我反复试了 /thoughts、/ryze/thoughts 都返回 405(Method Not Allowed)。外部 curl 测试又全部返回 401(需要认证),让人很困惑——为什么认证了就变 405?最后发现群晖 WebDAV Server 的路径结构比我想的多一层:实际的 WebDAV 根路径不是 /,而是 /homes/,用户主目录在 /homes/ryze/ 下面。完整的路径是 /homes/ryze/thoughts。这个问题的本质是我对群晖 WebDAV 的目录映射机制不够了解——WebDAV Server 套件默认通过 Apache 反向代理,路径映射规则和直接访问文件系统不一样。
(五)Android 明文 HTTP 的安全策略
Android 9 以上默认禁止 HTTP 明文流量。我的 NAS 内网地址用的是 http:// 而不是 https://,App 一发送请求就直接被系统拦截,报了 CLEARTEXT communication not permitted。解决方法是在 network_security_config.xml 里把 NAS 的域名加入白名单,允许明文传输。第一次只加了 localhost 和内网 IP 段,忘了加自定义域名 nas.ryze.fashion,导致又报了一次。
(六)Compose LazyColumn 的 key 重复闪退
这个bug 的报错非常明确——Key was already used——但触发时机很微妙。我用条目时间和内容的 hashCode 组合作为 LazyColumn 的 key,以为够用了。结果当同一条内容保存了两份(时间不同但 hashCode 可能相同,因为 hashCode 的碰撞概率高于直觉预期),App 直接闪退。解决方案是:对于没有唯一 ID 的数据,要么用 index 做 key,要么不要指定 key 让 Compose 按位置自动处理。
(七)Python 3.8 的类型提示兼容性
群晖自带的Python 是 3.8,不支持 list[dict] 这种 PEP 585 语法(3.9 才引入)。脚本一运行就报 TypeError: type object is not subscriptable。加一行 from __future__ import annotations 就解决了,但之前确实忘了 NAS 的 Python 版本比本地低。另一个 Python 问题是 common.py 的 docstring 里包含中文引号"随想杂记",被解析器当成了字符串结束符,导致 SyntaxError。这些问题都很低级,但组合在一起可能浪费半小时排查时间。
四、几个值得拿出来说的设计
做完之后回头看,有几个设计我觉得值得单独写出来。
(一)文件锁:最简单的并发控制
App 和 AI 脚本同时操作同一个原始记录文件,理论上存在竞争。但为了一个单用户的工具引入数据库或者消息队列太荒谬了。最终实现了一个基于文件锁的读写互斥:App 写入前先 PUT 一个空文件 .sync/write.lock 作为锁,写入完成后 DELETE 释放;AI 脚本运行前检查锁文件是否存在,如果存在就跳过本轮处理。锁获取加了指数退避重试(250ms → 500ms → 1000ms → 2000ms),第 3 次失败后强制执行僵尸锁清理。整个实现不到 60 行代码,但解决了核心的读写安全问题。这种简单粗暴的方案放在分布式系统里可能被骂,但放在个人 NAS 上就是最合适的。
(二)AI 调用的分级温度策略
不同层级的报告对AI 的要求是不同的。日报需要精准的分类和润色,温度设为 0.3,确保输出稳定一致;周报开始引入一些观察性内容,温度调到 0.5;月报和季报需要更多的洞察,温度 0.6-0.7;到了年报,我希望 AI 写得有温度、有故事性,温度直接开到 0.8。这个分级温度的设计是受了 OpenAI Cookbook 里关于不同任务适用不同 temperature 的建议启发的,效果很好——日报每次的输出风格高度统一,年报读起来像是一个了解你的人在帮你做年终回顾。
(三)降级策略:AI 不可用时不崩溃
DeepSeek 的 API 虽然便宜,但偶尔也会超时或返回非 JSON 格式的响应。common.py 里实现了一个 fallback_classify 函数:当 LLM 调用失败或 JSON 解析失败时,所有条目被归入"随想杂记"分类,原始内容原样保留。这保证了即使 AI 完全不可用,系统也不会丢失数据——用户第二天看到的是一份"未经整理"的原始记录日报,而不会是空白页。每层报告脚本都有类似的 fallback 机制:如果下层报告文件不存在或是空的,就跳过该层,不让错误向上传播。
(四)写穿缓存:要速度也要可靠
App 的数据层采用写穿缓存(Write-Through Cache)策略。用户保存一条记录时,数据先写 NAS(WebDAV),成功后才写入本地 Room 数据库。读取时优先展示本地缓存的最近 7 天数据(响应式 Flow,毫秒级刷新界面),同时后台从 NAS 拉取最新内容覆盖缓存。缓存写入失败不会影响主流程——用 Timber 打个 warning 日志就放过去。这种设计保证了:在线时体验快,离线时至少有 7 天数据可看,缓存崩了也不影响核心功能。
(五)部署与运维:群晖的"零成本"后端
部署过程比开发简单得多。NAS 端只需要五个步骤:创建 thoughts 目录结构、上传 Python 脚本、配置 DeepSeek API Key、pip install openai、在 DSM 任务计划里新建五个定时任务。定时任务的 Cron 安排避开了整点——日 1:17、周 2:23、月 3:37、季 4:41、年 5:53——这是故意把分钟数打散,避免多个任务同时触发抢 CPU。
文件传输遇到一个小插曲:SCP 在群晖上因 SFTP 子系统配置问题无法使用,最后用了 tar 管道的方式传输:本地 tar 打包 → SSH 管道 → 远端解包。这种方案在 DevOps 圈里算是老派做法了,但确实稳定。
Python 包安装也出了状况:直接从 PyPI 下载 openai 包超时(NAS 的网络环境访问海外源不稳定),换了阿里云的 PyPI 镜像后秒下。一个 pip install openai -i https://mirrors.aliyun.com/pypi/simple/ 解决问题。在国内部署 NAS 服务的读者建议直接把镜像源写进 pip.conf,后续更新省事。
(六)费用核算:一台永远不关机的电脑就是最好的服务器
整个项目的运行成本可以忽略不计。硬件用的是已有的群晖DS918+,不需要额外投入。DeepSeek API 每天处理 10 条记录的日报约 0.002 美元,周报月报季报年报的 AI 综述加起来每月约 0.01 美元,月费总计约 7 美分,折合人民币不到五毛钱。唯一的"成本"是 NAS 的 7×24 电费,但这台机器本来就常年开机跑着 Docker 和其他服务,多跑几个 Python 脚本基本不增加功耗。
四、写在最后
这个项目从构思到能用,实际编码时间大约一个周末。但最有意思的不是代码本身,而是它改变了我的记录习惯。以前打开备忘录需要克服一种微妙的心烦:想到要写、要分类、要整理,就算了。现在只需要几秒钟打几个字,甚至用豆包输入法直接说也行,剩下的事AI会帮我做。这种"零摩擦记录"的体验,比任何花哨的功能都更有价值。
做个人项目,我觉得最重要的品质是克制。功能列表里很想加的东西太多了——语音输入、Markdown 编辑器、桌面端同步、向量搜索——但我很清楚每加一个功能,维护负担就翻一倍。所以最后这个 App 的功能少到可以被一句话说清楚:打字,保存,第二天看日报。够了。
AI 编程工具在这次开发中帮了大忙,但主角永远是人。AI 擅长的是"已知的方案"——它能帮你快速写出 WebDAV 客户端的样板代码、帮你排查 Gradle 版本冲突、帮你生成 DeepSeek 调用的 prompt 模板。但做什么、不做什么、架构怎么搭、数据怎么流、哪些坑要绕开——这些判断是人的事,也是写代码真正的乐趣所在。
这个项目我会继续用下去。如果你也对"零摩擦记录"有兴趣,或者自己有一台 NAS 在吃灰,不妨也试试。代码思路都在GitHub上面了,点击查看原文就可以看到,剩下的就是花一个周末,给自己做一个懂你的思维外挂。
夜雨聆风