
终于又找得回来的会议:时间线版本
GeekBye v1.7.3 用一条真正的时间线取代了会议下拉列表——人性化的日期、搜索、一条告诉你正身处哪场会议的面包屑。而在这之下:一个自动选中的竞态、一张比它自己所属会议还早的截图,以及 28 分钟内的五个 Windows 修复。
到 2026 年 1 月,GeekBye 的用户遇到了一个历史记录问题。应用把他们的会议录得好好的、分析得好好的——然后归档进一个下拉列表。三条记录时,下拉列表没什么问题。到第十五场会议时,"上周二那场面试"就成了一场考古挖掘,要在一排排长得一模一样的条目里翻找。v1.7.3——从 12 月中旬到 1 月 16 日之间的 64 commits——就是让会议终于又找得回来的那个版本。
(一条记账式的说明,因为 git 历史是诚实的,哪怕版本号不是:并不存在 v1.7.2 这个 release tag。版本号确实提升过,但下一个真正发布的 tag 是 v1.7.3,于是 v1.7.2 时期的那批 Windows 修复就搭着这个版本一起发了出去。)
从下拉列表到时间线
最醒目的改动是一个 pull request:12 files、+640 行,其中大部分是一个全新的 381 行的 MeetingsTimeline 组件。下拉列表变成了一条可滚动的会议卡片时间线,分成四个分组——Today、Yesterday、This Week、Older(今天、昨天、本周、更早)——日期按照人们口头会说的方式呈现:Today, 2:30 PM、Yesterday, 2:30 PM,本周内是 Mon, 2:30 PM,再往前则是 Jan 5, 2:30 PM。Cmd/Ctrl+K 会聚焦一个按标题过滤的搜索框。会议详情视图变成了一个真正的导航页面,导航栏上的后退和前进按钮终于有了意义,面包屑也不再显示 "Meetings / Meeting"(会议 / 会议),而是显示 "Meetings / Interview with John"(会议 / 与 John 的面试)——超长时用省略号截断,因为总会有人给会议起个小说一样长的名字。
随之一起发布的还有两个小的体验改进:会议视图里加了一个删除按钮(删除后会把你带回时间线,而不是把你晾在一个已经不存在的页面上),以及自动跳转——当一次聆听会话带着实际的转录内容结束时,仪表盘会直接打开那场会议的详情页。挂断通话,就看到会议。内容为空的会话则会被悄悄删掉,不去弄乱时间线。
改进导航时亲手制造出来的竞态
这是 changelog 不会告诉你的部分。旧的会议列表有个习惯:每次加载时都自动选中最新的会话。当列表还是一个你很少去碰的下拉框时,这无伤大雅。而当会议变成一个你会主动导航的页面时,它就是致命的:点开第 5 场会议,一次后台 refetch 完成,列表"贴心地"选中了最新的那场会议——把你两秒钟前做出的选择覆盖掉了。修复不是写一个更聪明的守卫,而是把自动选中整个删掉。任何替用户做选择的列表,终有一天会和用户打起来。
删掉它暴露了第二个竞态。自动跳转会在会话结束的那一瞬间跳到那场会议——而这可能早于该会话把数据写完到本地 SQLite 数据库。跳得太快,你抵达的就是一场还不存在的会议。最终发布的修复是:进入详情视图时,在加载会话之前加 500 毫秒的延迟,而导航处理器还带着自己的一堆保险——如果仪表盘是刚创建的,就等窗口的 did-finish-load 再加 100ms;如果它本来就开着,则等 50ms。我们的解读是:会话结束时的跳转横跨了三个时序域——数据库写入、窗口加载、React 挂载——而每一个都各分到了一小段延迟。它能用,但我们也直说:一摞魔法数字是一笔老老实实记在账上的债,不是胜利。
比它自己所属会议还早的截图
这个版本还终结了一条假设:"截图属于某个会话,当且仅当它是在该会话期间拍的。" 逻辑上无懈可击。但是错的。真实用户会在按下 Start(开始)之前,就对着职位描述、议程、马上要讨论的代码按 Cmd+H。Report(报告)标签页严格按会话的时间区间去匹配截图,于是那些准备阶段拍下的截图,就从它们本来要服务的报告里消失了。修复是把匹配窗口向会话开始之前放宽 60 秒。(时间区间这套机制本身要更老——它在 v1.6.15 里悄悄上线;v1.7.3 的贡献是教会它:会议在开始之前就已经开始了一点点。)
28 分钟里的五个修复
这个版本的那段战事根本不在时间线里——而在 Windows 的引导教程遮罩层上,而且是一条教科书级的"修复上一个修复"链:同一个上午 10:50–11:18 之间的五个 commits。
GeekBye 的主窗口使用鼠标穿透——一个 hook 在每次鼠标移动时切换 Electron 的 setIgnoreMouseEvents,让点击可以落穿透明区域。在 Windows 的引导流程中,这个 hook 和教程遮罩层打了起来,到了仪表盘窗口出现的那一步,应用卡死了。第一个修复:让这个 hook 在引导进行期间什么都不做。缴械换和平。
只不过现在引导页的按钮不响应了,因为事件转发把点击都送去了遮罩层底下的仪表盘窗口。第二和第三个修复围着各个向导逐个摆弄 forward: false。然后才是那个找到真正根因的 commit:仪表盘窗口打开时,它自己的拖拽处理器会把主窗口重置回点击穿透模式——而第一个修复已经让穿透 hook 变成了被动的,这意味着再也没有任何代码能纠正这次干扰了。真正的修复把策略整个反了过来:在引导期间,这个 hook 现在会在每次鼠标移动时主动重新断言 setIgnoreMouseEvents(false)——恰恰因为外部代码随时可能把它重置。它还顺手删掉了此前按向导打补丁的 75 行代码。
即便如此还有第五个 commit,因为赢下输入之战不等于赢下 z-order 之战:遮罩层和仪表盘处在同一个置顶层级,而 Windows 会把获得焦点的那个窗口抬到上面。遮罩层被挪到了更高的窗口层级,并在仪表盘出现之后延迟地把自己重新抬起来。这条弧线值得记住:一个缴了你唯一守卫者的械的被动修复,比原来那场架更糟。
这个版本教会我们的三件事
- 导航升级会把状态 bug 逼出水面。 自动选中的竞态在 v1.7.3 之前就存在;只是下拉列表从来不会让你感觉到它。当会议变成一个你会刻意导航的页面时,每一段替你选东西的代码都成了 bug。
- 围绕用户意图的时间边界需要留余量。 会话、录音、报告——用户总会在机器的官方窗口前后各多动一点点。60 秒的截图缓冲只是一行代码,却反映了人们实际的行为方式。
- 永远别让你的纠错代码变成被动的。 Windows 卡死之所以花了五个修复,是因为第一个修复关掉了唯一能对抗外部干扰的机制。如果别的代码随时都可能破坏你的不变量,你的职责就是持续地重新断言它,而不是按兵不动。
关于 v1 故事的上一章——从这一套代码库构建两个品牌应用——见一套代码库,两个应用:如何在不分叉的情况下做白标(v1.7.1);整段弧线则见把软件打磨到完美地发布出去的解剖学。