
先放链接:GitHub:github.com/qingzo/dingmi 官网 dingmi.app。AGPL-3.0,Windows 10 / 11。
我是 uTools 的老用户,有一天数了数,天天用的只有三样:打开软件、记事到点提醒、随手记一笔。于是和 AI 一起做了叮咪 :按 Alt + 空格,一个框,打程序名就打开,写带时间的话就设好提醒,别的存成笔记。

这篇不讲怎么用(官网有使用说明),讲几处我觉得有意思的实现和踩过的坑。
一、技术栈和体量
| 部分 | 用的什么 |
|---|---|
| 桌面端 | Tauri 2(Rust + WebView2),前端 TypeScript + Vite,不用框架;编辑器 CodeMirror 6;数据 SQLite(rusqlite) |
| 同步协议 | 单独一个 crate,电脑端和服务器共用;Noise + WebSocket |
| 同步服务器 | tokio + hyper,编成一个 4 MB 的 musl 静态二进制,init 一下再 run 就能跑 |
| 手机 | 不做 App:服务器自带一个网页,esbuild 打成一个 HTML 编进二进制 |
| 加密 | Rust 用 RustCrypto(argon2、chacha20poly1305、crypto_box、hkdf)、snow;网页用 @noble/ciphers、hash-wasm、tweetnacl |
公开部分 Rust 大约 2.5 万行,TypeScript 2.3 万行;前端 600 多个测试,Rust 200 多个。安装包 8 MB。
前端不用框架是故意的:窗口很多(快捷框、提醒卡片、笔记窗口、桌面组件、主窗口......),每个都很小,手写 DOM 加一个共用的样式和组件文件就够了,打出来的包小,WebView2 里启动也快。
markdown
┌──────── 叮咪(Tauri)────────┐
│ WebView2:快捷框 / 卡片 / 笔记 │
│ Rust:SQLite、调度、启动器、 │ Noise over TCP / wss
│ 加密、同步引擎 ───────┼──────────────► dingmi-server
└──────────────────────────────┘ (只存密文)
▲
手机浏览器(服务器发的网页,同一套加密)────────┘
二、不靠 AI 的时间解析
「一句话设提醒」最核心的是认时间。可以接 AI(填了 DeepSeek 的 Key 会更准),但我希望不填 Key、不联网也能用 ,所以有一份本地规则:parse.ts,700 多行。
难的不是「明天下午三点」,是这些:
- 「明天下午3点前把表发我」:说了「前」,要提前叫(13:00 提醒,卡片上写截止 15:00),不然三点整才想起来就晚了;
- 「每月最后一个周五」「工作日9点」「每隔七天」;
- 「每年农历八月十五」:要换算成公历,每年都不一样;
- 「改天请老王吃饭」:说不清的,先定一个时间,并且明确告诉用户「没听清,先定在明天 9:00」。
规则这种东西,改一处坏三处。我的办法是把测试句子当需求文档 :parse.cases.ts 里 386 句,每句写好应该认成什么,发现认错的句子先加进去再改规则:
ts
// 没写 now 的都按 2026-11-05(周四)10:00 算
c("明天下午3点开会,提前半小时提醒我", "开会", "11-06 14:30", { detail: "事情本身在 11月6日 15:00" }),
c("每年农历八月十五回家", "回家", "2027-09-15 09:00", { repeat: lunar(8, 15) }),
c("傍晚去遛狗", "去遛狗", "11-05 18:00"),
和 AI 一起改规则时,这份清单就是护栏:AI 改完一跑,386 句里哪句变了一目了然。
三、启动器:拼音、多音字和「刚装好时怎么排序」
找程序用的是 COM:开始菜单里的程序、微软商店应用、桌面快捷方式,后台线程里找好放在内存里,图标缓存到本地。
两个细节:
- 多音字 :程序名按字拆成一段一段,每个汉字列出所有读音(
pinyincrate),匹配时任一读音对上就算。不然「重」「行」开头的程序打拼音找不到。 - 冷启动排序:刚装好时还不知道你常用什么,排序就很蠢。Windows 自己记着每个程序打开过几次(开始菜单「最常用」用的那份),先拿它来排,用一段时间以后换成叮咪自己记的次数。
四、读别的程序里选中的文字
「选中一段文字,按快捷键,文字自动带进来」,实现上就是模拟一次「复制」,读剪贴板,再把剪贴板恢复原样。
坑在于模拟哪个键:Ctrl + C 在命令行窗口里是中断程序 ,用户在终端里选了一段字、按了快捷键,正在跑的程序就被你掐了。所以模拟的是 Ctrl + Insert,效果一样是复制,但终端不把它当中断。
五、加密同步:服务器只是一个「加密信箱」
叮咪收费的地方只有官方同步,「我们解不开你的数据」这句话必须能查,这也是开源的主要原因。设计要点:
- 所有记录用一把随机的主密钥加密(XChaCha20-Poly1305,附加数据是「记录编号|版本号」,服务器把密文挪到别的记录上会解密失败);
- 主密钥锁在「盒子」里:盒子 1 要同步密码(Argon2id)+ 一把只在设备上的空间密钥才打得开;盒子 2 用恢复码。服务器上只有盒子,没有钥匙。多一把空间密钥的好处是:数据库被偷了,也没法拿着盒子离线猜密码;
- 连接再套一层 Noise(
Noise_IK_25519_ChaChaPoly_BLAKE2s),服务器只认登记过的设备公钥; - 手机网页和电脑端是两套实现(Rust / TypeScript),用同一组测试数据核对算出来的结果一致。
服务器能看到什么、防不住什么 (比如手机网页是服务器发的,这一层要信任服务器),我写在仓库的 docs/加密和同步.md 里,欢迎挑刺。
开源和收费怎么共存:开源的服务器里留了一个 Ext trait 当扩展点,官方版在上面加兑换码、到期、后台这些,不公开;开源版走默认实现,功能是全的,可以自己搭。
六、踩过的坑:一个 \r 让手机网页白屏
手机网页带着严格的 CSP,内联脚本靠 sha256-... 哈希放行,哈希是服务器启动时按网页原文算的。
上线以后手机打开一片空白。查下来是:Windows 上 git 检出来的文件是 \r\n 换行 ,网页被 include_str! 原样编进程序;浏览器解析 HTML 时会把脚本里的 \r\n 规范成 \n 再算哈希,和服务器按原文算的对不上,整段脚本被拦。Linux 上编就没事,所以本地测试一直是好的。
修法是编进去的时候先把换行统一成 \n,再算哈希。教训:「同一份代码在哪台机器上编」也是变量,涉及哈希、签名的东西,要有测试在 Windows 上编出来的产物上跑一遍。
七、和 AI 一起做,是怎么分工的
坦白说,大部分代码和文档是 AI(Claude)写的。我的工作是:
- 定需求:每个功能先写需求,写到「这个按钮点了会怎样、出错了说什么」;
- 看设计稿:界面先出设计稿,定了再开发;
- 定取舍:比如「不填 AI Key 也要能用」「服务器拿不到钥匙」这种原则;
- 自己用、自己测:每一版先在自己电脑上用一段时间。
我觉得最关键的是测试:AI 写得快,也改得快,没有测试兜底,改一处坏三处是常态。时间解析那 386 句、防滥用的攻击测试(「拿 ..\ 的编号存附件」「一直发 Ping 不收」这类,先写成攻击、以前的程序过不了、改了以后过得了),都是为了让 AI 改代码时有护栏。还专门做过几轮安全审查,修出来的问题都留了测试。
最后
- 只收 Issue,不收 PR(版权要在我一个人手里,官方同步才能不开源),好的想法我自己实现;
- 安全问题请走 GitHub 的私下报告;
- 只有 Windows,没有插件市场,不打算做成 uTools 那样的平台。
GitHub:github.com/qingzo/dingmi,觉得有意思的话点个 Star。也想听听:你会用什么技术栈做这样一个常驻后台的小工具?