给聊天输入框做草稿持久化,第一个要回答的问题是:这份没发出去的文字,算谁的?全局一份,还是每个会话一份?
今天打开任何一个主流 IM 或 AI 对话产品,答案都是一样的:草稿跟着会话走。这个答案现在看起来像常识,但它是各家产品花了将近十年、踩过若干真实事故才收敛出来的。这篇文章把这段演变的关键节点摆出来,每个节点都有据可查。
起点:草稿解决的是"被打断"
2016 年 6 月 14 日,Telegram 在官方博客发布 Drafts 功能。博客给出的理由很直接:messenger 里有很多人抢你的注意力,你输入到一半的重要消息被打断,然后就忘了。
Telegram 第一版就做对了三件事,后来证明这三件事都是共识的组成部分:
- 草稿按会话存,API 里的
DraftMessage构造器从第一天起就是 peer 维度的; - 聊天列表里直接显示未完成的草稿,有草稿的会话自动置顶;
- 草稿云端同步,手机上打了一半,电脑上接着打。
值得注意一个反面细节:同时代的多数 IM 根本没有草稿。VentureBeat 当时的报道特意提了一句,这个功能"在很多聊天应用里都没有"。Telegram 的方案激进在一开始就回答了归属问题:草稿属于某个会话,所以它出现在会话列表里,跟着会话跨设备走。
Slack:把草稿做成一个"区"
Slack 在 2019 年 6 月的官方博客介绍了 Drafts:写到一半没发的消息,会出现在频道侧边栏一个专门的 Drafts 区域。帮助文档里写得更清楚:开始写消息而不发送,它会自动保存为草稿;草稿按 channel、DM、thread 各自归属。
Slack 补上的那块拼图是可发现性的第二种形态。Telegram 用列表内标记加置顶,Slack 用一个独立的 Drafts 视图收编所有会话的草稿。两条路线后来都被广泛采用,Mattermost 甚至两个都做了。
Zulip 的反面教材:光"存下来"不够
开源社区里 Zulip 的草稿演变最能说明问题。Zulip 很早就有关闭输入框自动存草稿的机制,但存完只能点 Drafts 按钮去列表里找。2021 年 5 月,issue #18555 列出了这个设计的三个实害:
- 回到草稿要点好几下,而这恰恰是最常见的操作;
- 新用户根本不知道有草稿功能,以为自己写了一半的内容丢了,这是真实的用户反馈;
- 草稿"泄漏":用户没意识到自己有未完成的稿子,直接写新消息,草稿列表堆满废稿,有用的反而找不到。
issue 里有一句话点破了共识的存在:"其他聊天产品(Slack、Discord)默认把草稿恢复到输入框。"到 2021 年,"自动保存 + 默认恢复"已经是拿来当论据的既成事实,不需要再论证。
这个 issue 2024 年 4 月才关掉。Zulip 最终的行为是:输入框打开时,自动恢复当前会话最近编辑的那条草稿;清空文本即删除草稿。从"存下来就行"到"无感恢复",Zulip 走了三年。它还提供了一个粒度上的参照:Zulip 的会话是 stream + topic,草稿也就跟着细到 topic。粒度跟着产品对"会话"的定义走,这是 per-session 共识里容易被忽略的一条。
Mattermost:粒度、同步、汇总视图的开源样本
Mattermost 是开源实现里资料最全的样本,它的代码和 PR 记录能看清共识落地的完整路径。
粒度上,Mattermost 的草稿 key 分两种:draft_{channelId} 存频道输入框,comment_draft_{rootId} 存线程回复框。和 Zulip 的 topic 一个道理:草稿挂在"消息将被发往的目标"上,线程也是一个目标。
持久化上,2021 年 7 月的 commit MM-37048 处理了一个真实 bug:草稿从 Redux persist 自动 rehydrate 改成手动从 localStorage 恢复,修掉"输入完立刻刷新页面丢草稿"的问题。同一改动里,组件在 beforeunload 和切换频道时都会先保存旧频道的草稿。
2022 年 11 月,PR #8869(+5540 行,113 个文件)合入了全局 Drafts 页,把所有频道和线程的草稿收进一个中央视图。到 v7.7(2023 年初),草稿默认同步到 Mattermost 服务器,web 和桌面端都能拿到,至此追平了 Telegram 2016 年的云同步。
这条时间线值得单独看:本地按会话存(更早)→ 修持久化可靠性(2021)→ 中央汇总视图(2022)→ 服务端同步(2023)。每步都有明确的用户问题在推动。
反例:共享草稿是一份事故报告
共识为什么是对的,反过来说更清楚。2026 年,meshmonitor 的 issue #4183 报告了一个事故:DM 视图和频道视图共享同一个 React state 当草稿,在私聊里写了一半切到频道,草稿还留在输入框里,发送就直接发去了公开频道。
修复 PR #4185 的描述里写着定性结论:"A private DM draft could even be sent to a public channel --- a real correctness/privacy bug."共享草稿不是体验瑕疵,是隐私事故。修复方式也符合共识:按会话标识(tab + 节点/频道)隔离草稿,目标变化时统一处理。
类似的问题在别的项目里以较轻的形态反复出现。alkem-io 的 PR #10089 修"关掉面板丢草稿",顺手修了两个连带 bug:草稿串到下一个会话(靠按会话 key 隔离解决)、发送失败时输入框已被清空(改成发送成功才清)。Freegle 的 PR #1031 记录的用户抱怨更朴素:"我在一个聊天里写回复,点去另一个聊天想复制粘贴点东西,回来草稿没了。"
这些报告的共性是:没有一份是抱怨"草稿为什么不能全局共享",全是抱怨草稿丢了、串了、发错地方了。共识是被这种抱怨喂大的。
AI 时代的新问题:会话还不存在的时候
AI 对话产品给这个老共识加了一个新场景:用户输入的时候,会话往往还没创建。草稿挂在哪?
OpenHands 的 use-draft-persistence.ts 是目前最完整的开源答案,它把场景拆得很细:
- 有会话 ID,草稿存 localStorage,按会话 key;
- 在首页(还没有会话),草稿存 sessionStorage 的一个固定 key,标签页关掉就丢,理由是没归宿的内容不值得长期留;
- 新会话初始化用临时 task ID,草稿不能跟着丢,等真实会话 ID 就绪后把草稿迁移过去;
- 保存前校验捕获的会话 ID 没过期,防止快速切换时把 A 的草稿写进 B。
first-tree 的 PR #1165 补充了两个工程细节:草稿 map 封顶最新 100 条做 LRU;只有附件没有文本的状态不持久化,避免系统自动带的默认选项伪装成用户草稿。
alkem-io 则贡献了另一条边界:草稿按 { [userId]: { [conversationId]: draft } } 嵌套,登出时整个清掉,因为草稿是私密消息内容;发送成功才删草稿,而不是按下发送就删,他们专门修过后者导致的静默丢内容。
收敛出来的五条
把这些节点叠在一起,共识可以拆成五条互相独立的判断,每条背后都有上面的事实支撑:
- 归属粒度跟着会话定义走。IM 是 channel/DM,Zulip 细到 topic,Mattermost 细到 thread,AI 产品是 conversation。全局共享一份草稿的方案在每一处都已被证伪。
- 自动保存,默认恢复。Zulip 的三年弯路证明,"存了但要用户自己记得去找"等于没存。
- 会话列表给出可见性。Telegram 的列表标记加置顶、Slack 的 Drafts 区、Mattermost 的全局 Drafts 页,形式不同,目的一样:草稿找得回来才有意义。
- 发送成功才清除。alkem-io 用实际 bug 确认过,按下发送就清空会在失败路径上静默丢内容。
- 无会话场景给固定归属,创建后迁移。这是 AI 产品新补的一条:home 页或新会话输入时挂固定 key,会话创建成功把草稿带过去。
回头看,这个共识没有过一次公开的"定义时刻",它是 Telegram 立下的原型、Slack 补齐的形态、Zulip 和 Mattermost 用开源进程反复验证、再由一串丢草稿的事故报告反向加固出来的。今天再给任何一个聊天输入框设计草稿,不按这五条做,需要的论证成本比按这五条做高得多。
参考来源
- Telegram 官方博客:Drafts, Picture-in-Picture, and More(2016-06-14)
- Telegram API 文档:Message drafts
- Slack 官方博客:In case you missed it: save drafts(2019-06-11)
- Slack 帮助中心:Send and read messages
- Zulip issue #18555:Restore drafts by default
- Zulip PR #22127:Create a section to show drafts from the current conversation
- Zulip 帮助中心:Draft messages
- Mattermost commit MM-37048:drafts localStorage rehydrate(2021-07-19)
- Mattermost PR #8869:global drafts page(2022-11-24 合入)
- Mattermost 文档:Draft messages(v7.7 起服务器同步)
- meshmonitor PR #4185:scope compose draft per-conversation
- alkem-io commit c284653:keep unsent chat drafts
- Freegle PR #1031:Persist the in-chat compose draft across chat switches
- OpenHands:use-draft-persistence.ts
- first-tree PR #1165:cache unsent composer drafts per chat