跳到正文
安时安其时,成其事
下载
设计思路

每条原则后面都跟着一个反例。

「我们坚持隐私优先」是空话。下面每一条都带上「不这样做会发生什么」, 而那些反例不是假想的——它们是这个项目在开发过程中真的撞过的。

数据在哪

本机一个 SQLite 文件。迁移前自动备份。

要账号吗

不要。多端同步也不需要注册。

谁能读到

同步一律 E2EE,口令只存本机钥匙串。

模型 Key

填在你自己机器上,不代管、不中转。

01

Local-first:数据先落本机,同步失败绝不影响读写

所有数据落在本机一个 SQLite 文件里。不注册、不联网也是完整可用的——离线不是降级模式,而是默认模式。

同步是加上去的一层,它坏掉的时候本地读写不受任何影响。反过来做(先写云端、本地当缓存)的代价是:网一断,你连自己今天要干什么都看不到。

代价:没有「换台电脑打开网页就能用」这回事。要多端就得配同步,而配同步意味着你得自己有个地方放那些加密字节(自建 CouchDB、任意 WebDAV,或者一个 iCloud / Dropbox 目录)。

02

截止是承诺,排期才是安排

只有一个日期字段的待办工具,逼你在「今天要做」和「周五必须交」之间选一个填。选前者,deadline 就丢了;选后者,今天的清单里它不出现。

安时把它们分开,并且在日历上分两种形状画:截止是一条虚线边界(它是一刻,没有时长),排期是一个虚线块(它是一段,可以有好几段,可以拖着改)。排期越过截止时,界面会明确标出来。

代价:多了一个字段要填。所以新建任务时会按截止日与预估耗时提议一段排期,避开当天已有安排;两者都没填就不提议——凭空排一段是替你做了个没依据的决定。

03

AI 不直接写库,也不读库

模型只做两件事:出关键词、出草案。查库定位、时间算术、冲突检查、落库,全部由核心做,并且要等你按确认。

为什么这么死板:曾经把「明天上午 11 点提醒我」交给模型整理,它给出的是 2025-01-09T11:00——时刻对,年月是八个月前。那个值格式完全合法,界面按「月-日」印出来看着正常,确认之后才发现任务落到了去年一月。现在核心拿你原话里的日期词自己算,改过会在草案卡上说明。

也不让它读库:让模型自己查「明天有哪些事」,它会编出几条很像真的安排。清单一律由核心查库给出,模型只负责把「明天」翻成一段时间范围。

代价:模型能做的事比「让它全权代理」少。换来的是:确认之前库里什么都不会变,而且草案卡上写着这一下到底会发生什么(「加这段排期」而不是「确定」)。

04

多端对等,端到端加密,口令永不上传

每台设备都是完整的主副本,没有主从。核心维护一条 append-only 的变更日志,按 HLC 做字段级收敛——两台设备同时改同一条任务的不同字段,两边的改动都留得下来。

插件只负责把已经加密好的字节搬到远端再搬回来。它看不懂内容,也不做任何合并判断。加密口令存在本机系统钥匙串里,永不同步、永不上传。

代价:口令丢了,远端数据就解不开了(本地数据不受影响)。这是端到端加密的定义,不是实现缺陷——能帮你找回口令的系统,也就能读你的数据。

05

插件独立进程,展示面是「声明式数据 + 宿主渲染」

插件跑在独立子进程里,崩了、卡了、拉不到数据,都不影响本地写库。权限逐项声明,界面上翻成人话给你看。

插件的展示面不开 HTML / iframe / 自定义布局的逃生口。它给的是「(标签, 值, 语义档位)」,颜色、字体、排版、项数上限全归宿主。留了那个口子,所有插件都会走那条路,封闭布局就白定义了——而 iframe 里的 fetch 还会绕过插件声明的联网权限,插件的联网面从此不可审计。

代价:插件作者不能把自己的品牌色和排版搬进来。换来的是:装十个插件,浮窗看起来还是同一个软件,深色主题也不用指望每个插件都适配。

07

一个强调色,四种语义

全应用只有一个强调色(浅色 #C2410C,深色 #E27844),只用在四件事上:今天、选中、高优先级、逾期。铺满了它就不再是信号。

分层靠留白与极浅底色,几乎没有边框。标题用衬线(Newsreader + 宋体),正文与所有时间数字用无衬线 / 等宽——等宽不是装饰:不开 tabular-nums 的话,一列日期里 11 比 18 窄一截,六周下来左边缘参差不齐。

颜色留给「这条是哪儿来的」:每个日历源一个色,从一套按 CIELab 色差生成、并由单元测试守着的调色板里挑(两两 ΔE > 10、离强调色 > 30、浅色与深色底上都看得见)。手挑的第一版里有几组 ΔE 只有 4,根本认不出差别。

08

常驻入口比功能多少更重要

一个待办工具真正的竞争对手是「先记在便签上」。所以浮窗、全局热键、托盘今日快览是一等公民,而不是设置页里的一个开关。

浮窗不滚动:一个能滚的浮窗就是第二个主界面。它是一列有上限的小组件,你自己决定装哪几张、什么顺序。

提醒到点由本机发系统通知。桌面系统通知没有动作按钮,所以「完成 / 推迟 / 知晓」落在浮窗顶部与标题栏的提醒收件箱里——两个出口,因为浮窗热键是可以被用户关掉的。

视觉语言

形状承担语义,颜色承担来源

这条约束的实际后果是:给任务挑了颜色之后,排期块仍然是虚线框、截止仍然是虚线边界。 形状才是「这是我要做的事」的载体,颜色只换色相。

产品周会

事件 · 实心块

已经约好的一段时间:会议、行程、别人也在的事。它的颜色跟着所属日历走。

定稿 · 14:00 – 15:30

排期块 · 虚线框

你打算什么时候做这件事。虚线是「计划」的意思——它可以改、可以拖,也可以有好几段。

18:00 交排期表

截止 · 一条边界线

最晚要做完的那一刻。它没有时长,所以不画成块——画成块会被读成「这天要做一整天」。

任务 · 优先级在圈上

任务 · 圆勾圈

圈的描边色是优先级(P0 最重到 P3 最轻),不是你给任务挑的那个颜色。

可扩展的边界

六个扩展点,各有各的授权模型

「日期标记」和「日历叠加层」在技术上几乎是同一件事,但授权模型刻意分开: 前者是公开知识(谁算都一样,装上即生效),后者是你的个人数据(默认关闭,逐个启用)。

扩展点它能做什么
同步源
sync
把外部系统的任务或日程搬进安时。task 是双向的(完成状态会回写),event 目前是 pull-only——外部日历远端权威,落成只读日历。
通知渠道
notify
提醒到点时除了本机系统通知,还能发到别处。内置系统通知与第三方渠道走的是同一条路径。
同步后端
replica
多端同步的传输层。插件只负责把一串已经加密好的字节搬到远端再搬回来,合并判断全在核心,插件看不懂内容。
日期标记
dayMarks
给某一天贴一个短标签(节假日、纪念日)。这是公开知识——谁算都一样,所以装上即生效。
日历叠加层
calendarOverlay
往月视图格子右上角贴角标、往视图右上角贴汇总、往侧栏负载卡里加几行统计。这是你的个人数据(打卡、请假),所以默认关闭、逐个显式启用,关着的时候,安时根本不去调那个插件。
生命周期钩子
hooks
订阅 task.completed / task.reopened / task.updated 这类事件,用来把「这条完成了」回写到外部系统。它是扩展点里唯一一个「只收不画」的:不贡献任何界面,所以也没有展示面的开关。
浮窗小组件
widget
往浮窗那一列卡片里加一张。插件给的是「(标签, 值, 语义档位)」,颜色、字体、排版、项数上限全归宿主。
关于这些代码

人和模型一起写的,而且记了账

这个项目几乎每一行都是人和模型一起写出来的。每次提交都在信息里署一行Model: <model-id>,所以「哪个模型写了多少」不用另外记账—— 应用的设置 → 关于页里那张占比图就是 git 历史本身,构建时统计成快照。 占比按改动行数算(新增加删除),删代码同样是产出;没有署名的提交落进「未标注」,不伪造归属。