改了默认快捷键老用户拿不到,因为它存在 localStorage 里

2026-08-09

本文描述的是 OpenCut 旧版(classic,仓库 OpenCut-app/opencut-classic)的行为。该代码库已归档、不再维护,opencut.app 线上目前仍运行该版本;重写版(仓库 OpenCut-app/OpenCut)正在开发中,功能与操作可能变化。核对日 2026-08-09。

这篇讲的是一类特别容易被误判成”缓存没清”的问题:你在代码里把某个功能的默认快捷键改了或加了,新拉起来的干净环境一切正常,而自己那个用过一阵的环境死活没反应。在 OpenCut classic 里,这件事有明确的文档解释——快捷键是持久化在 localStorage 里的,改 defaultShortcuts 只对全新安装生效,要让已有用户拿到新默认值,得单独写一个 keybindings 迁移。

先把边界说清楚:本文所有事实来自 classic 仓库的 docs/actions.md 与 README,我们没有编译或运行过任何版本的 OpenCut,下面不会出现任何界面描述、操作手感或”我这边试了一下”。能给你的是文档语义与代码路径,以及基于这两者能推导出的判定动作。

一、现象长什么样

场景是这样的:你按 classic 的 docs/actions.md 给编辑器加了一个 action。第一步是在 src/lib/actions/definitions.tsACTIONS 对象里加一条,文档给的模板是:

"my-action": {
    description: "What it does",
    category: "editing",           // playback | navigation | editing | selection | history | timeline | controls
    defaultShortcuts: ["ctrl+m"],  // 可选
    args: { someValue: "number" }, // 可选,只在需要参数时
},

category 是个枚举,文档列出的七个取值是 playbacknavigationeditingselectionhistorytimelinecontrolsdefaultShortcutsargs 都标了可选。

代码这么写完,构建也过了,功能本身也接上了。问题出在 defaultShortcuts 这一行:在一个从来没打开过编辑器的环境里,这个默认快捷键是生效的;而在一个已经用过的环境里,它像不存在一样。同一份代码,两种结果——这就是这类 bug 最迷惑的地方,因为它看上去像构建缓存、像 dev server 没热更新,像任何一个跟”环境脏了”有关的东西。

顺带说一句反直觉的点:这类问题往往不是”改的地方不对”,而是”改对了但没到用户手上”。写代码的人心里的模型是”默认值 = 运行时读到的值”,而带持久化配置的产品里,这个等号早就断了。

二、怎么确认就是它

docs/actions.md 在讲”新增一个 action”的注意事项时,把理由写得很直白:快捷键持久化在 localStorage 里,新的默认值只对全新安装生效。所以这条判定其实很容易做,不需要猜。

动作一:拿一个干净的浏览器环境对照。 classic 的本地开发按 README 是 bun installbun dev:web,应用起在 http://localhost:3000。同一个 dev server 不动,换一个从没访问过这个源的浏览器环境(新建 profile 或隐私窗口都行)再打开一次。如果新环境里默认快捷键有效、原环境无效,那就完全对上了文档说的”只对全新安装生效”这个语义,基本可以定性。

动作二:去看状态是从哪儿读的。 打开 src/stores/keybindings-store.ts,确认快捷键映射的来源是持久化存储而不是每次从 ACTIONSdefaultShortcuts 现算。这一步是从代码侧坐实动作一的结论,而不是只靠现象归纳。

动作三:查有没有对应的迁移。src/stores/keybindings/migrations/ 目录下有没有一个覆盖你这次改动的迁移文件。文档给的命名形式是 vN-to-vN+1.ts。没有,就是缺了。

这里有个必须说明的地方:docs/actions.md 只说了”持久化在 localStorage 里”,没有给出具体的存储 key 名,我们也没有运行过编辑器去看真实的存储内容。所以别照抄任何地方看来的 key 名去 devtools 里搜,自己顺着 keybindings-store.ts 的持久化配置找到它——这是唯一可靠的路径。

三、文档给出的处置

docs/actions.md 的处置只有一句话,但这句话是硬性的:只要你给 action 写了 defaultShortcuts,就必须同时写一个 keybindings 迁移,文件放在 src/stores/keybindings/migrations/vN-to-vN+1.ts

也就是说,在这套代码里,“加默认快捷键”这个动作天然是两件事,而不是一件:

  1. src/lib/actions/definitions.tsACTIONS 里声明新的默认值——它决定全新安装拿到什么;
  2. src/stores/keybindings/migrations/ 下加一个版本迁移——它决定已有用户的那份 localStorage 数据怎么升到新版本。

漏掉第二件,代码是对的、测试环境是对的,只有存量用户是错的,而存量用户恰恰是最不会跑来给你复现的一批。

文档到这里就停了:它给了目录位置和 vN-to-vN+1.ts 这个命名形式,没有给迁移函数的签名、版本号变量名,也没有给一份完整的迁移示例。所以这里不编,正确做法是打开 src/stores/keybindings/migrations/ 下已经存在的迁移文件,照着它的结构写你这一版——仓库里已有的写法就是这套代码的权威示例。写迁移时要考虑的语义也很常规:老用户可能已经把这个键位改成了别的,也可能这个键位已经被他自己占用了,迁移逻辑要不要覆盖用户的自定义,是个产品决定,不是文档能替你回答的。

四、处置后怎么验证

验证要分两类环境做,缺一类都不算验过:

全新安装那一路。 用一个从没访问过这个源的浏览器环境打开本地起的应用(默认 http://localhost:3000),确认默认快捷键按 defaultShortcuts 里写的值生效。这一路在你写迁移之前就是通的,验它只是为了确保迁移没有把原本正常的路径带坏。

存量升级那一路。 这一路才是真正要验的:先在没有你这次改动的代码上把编辑器用起来,让 localStorage 里落下一份旧版本的快捷键数据;再切到带迁移的代码,不清除任何存储地重新加载。这时候新的默认快捷键应该已经生效。

最容易出错的一步就在这里:很多人验的时候顺手清了存储或者换了新窗口,那等于又回到了”全新安装”这一路,迁移写没写都能过。验迁移的前提是那份旧数据必须还在。

还有一处值得单独看:如果你的迁移选择了”用户已自定义过就不覆盖”,那么验证时要额外准备一个”用户改过键位”的旧数据,确认迁移没有把人家的设置抹掉。这个分支不做,等于把一次默认值调整变成了对存量用户配置的一次静默重置。

五、什么情况说明不是这个原因

这一节是这类排查里最该保留的部分,因为”看起来像”的岔路不止一条。

岔路一:全新环境里也不生效。 如果你换到干净环境后默认快捷键依然没反应,那就跟 localStorage 完全无关了。docs/actions.md 同一节还列了另一个坑:快捷键用到特殊键(非字母数字)时,要检查 src/stores/keybindings-store.ts 里的 getPressedKey,缺了就补一个 case,文档给的例子是 if (key === "escape") return "escape";。这条和迁移那条是两个独立问题:迁移问题的特征是”新环境好、老环境坏”,getPressedKey 缺 case 的特征是”两边都坏,且只坏在特殊键上”。先看这一条判据,能省掉一整轮无效排查。

岔路二:action 本身没接上。 如果快捷键、按钮、右键菜单三条入口全都没反应,那问题多半不在快捷键这一层。docs/actions.md 对 Actions 的定位写得很明确:它是用户发起操作的触发层,负责把键盘快捷键、UI 按钮、右键菜单连到编辑器功能上。三条入口一起失灵,说明是被触发的那一端有问题,而不是触发方式没配上。

岔路三:category 写了枚举外的值。 七个合法值就是上面那组,写别的会怎么表现文档没说,我们也不去猜,但排查时顺手核一眼成本极低。

岔路四:你改的根本不是这个仓库。 OpenCut 现在有两个代码库,状态完全不同:OpenCut-app/opencut-classic 已归档、不再维护,opencut.app 线上跑的是它;OpenCut-app/OpenCut 是重写版,README 明写正在从头重写,其中提到的 Editor API、第三方插件、跨三端一套代码、MCP server、无头模式、编辑器内脚本标签页,全部是官方 README 声明的计划,尚未发布,我们没有见到可用实现。本文引用的 docs/actions.mdACTIONSkeybindings-store.ts、迁移目录全部来自 classic,别拿这些路径去重写版仓库里找。

最后:这条经验不只属于 OpenCut

docs/actions.md 那句注意事项,剥掉 OpenCut 的外壳之后是一条通用结论:在任何把用户配置持久化下来的产品里,“改默认值”和”让老用户拿到新默认值”是两件独立的工作,后者需要一次显式的数据迁移。

快捷键只是最常见的载体,同样的形状还会出现在默认主题、默认导出参数、默认面板布局、功能开关的初始状态上——凡是”首次运行时写一份到本地,之后就只读本地”的字段,都吃这一套。判据也一样好用:新环境正常、老环境异常,先去找持久化层,别在默认值那行代码上反复看。

延伸阅读


本文依据 OpenCut 官方仓库(github.com/OpenCut-app/OpenCut 与已归档的 github.com/OpenCut-app/opencut-classic)的 README、docs/ 架构文档、package.jsonCargo.tomlchangelog/ 整理,核对日 2026-08-09。本文内容为仓库源码与文档口径,我们没有编译或运行过任何版本的 OpenCut。

本文描述的是 OpenCut 旧版(classic)的行为。该代码库已归档、不再维护,opencut.app 线上目前仍运行该版本;重写版正在开发中,功能与操作可能变化。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。