CC Switch 删除报错,东西其实已经删掉了
删一条 Skill 备份,收到的是「删除失败」,过一会儿再看,那条备份已经没了。
这种事最难受的地方不是删没删掉,而是你不知道该信哪个:信提示,就得再点一次删除;信直觉,又怕留了个删了一半的目录在那儿。CC Switch 的前端在这件事上写了一段专门的处理,而且把理由用英文注释写进了代码里。这篇就把那段拆开,顺带给出一个不用翻磁盘就能做的判定。
本文依据的是 v3.20.1 这个快照(对应仓库 commit 3217f725),核对日 2026-08-31。所有结论都来自静态读源码:我们没有编译过这个项目,没有安装、也没有运行过这个桌面应用,因此下面不会出现任何关于界面长什么样、点起来什么感觉的描述。下文提到的行号都对应 v3.20.1,随版本会飘。
这段代码在哪,它防的是什么
入口是 src/components/skills/UnifiedSkillsPanel.tsx 的 handleDeleteBackup,535 到 604 行。这个函数不直接删,它先摆一个确认对话框的状态,真正的动作全在 onConfirm 里。
关键在于 onConfirm 内部对错误的态度。代码注释写得很直白:remove_dir_all 有可能已经把目录删掉了,却仍然返回一个错误。也就是说,后端返回的那个 Err 并不能反推出「东西还在」。前端如果照着 Err 直接翻译成「删除失败」,就有一定概率在说假话。
这一层区分值得单独强调:「调用返回了错误」和「这次操作没有生效」是两件事。 大部分前端代码把这两件事当成一件,因为绝大多数接口确实是同进同退的;而目录删除这类操作天然可能做到一半。这段代码的全部复杂度都是从这一句事实长出来的。
三重处理:它到底做了什么
| 处置 | 位置 | 解决什么 |
|---|---|---|
| 成功和失败都强制拉一次权威列表 | 560-571 行 | 不拿本地状态当结论,去问一遍「现在到底还剩哪些备份」 |
| 刷新自己失败,不许改写删除的结论 | 564-571 行 | 一次已经完成的删除,不能因为刷新挂了就被报成失败 |
| 删除报错、但权威列表里已查不到这条 | 577-584 行 | 把那个已经过期的确认对话框状态清掉 |
第一条的写法是 refetchSkillBackups({ throwOnError: true }),它不是「让缓存失效、等下次自动重取」,而是当场显式拉一次并要求出错就抛。第二条对应的英文注释原文是 “A refresh failure must not turn a completed deletion into a false ‘delete failed’ report”——刷新失败只往控制台打一条错误日志,既不覆盖原始的删除错误,也不把已完成的删除翻译成失败。
第三条是三条里最反直觉的:删除报了错,代码却把确认对话框关掉了。原因是它拿到的权威列表里已经没有这条备份,那个还等着你二次确认的对话框,确认的对象已经不存在了,留着只会诱导你再删一次。
但请注意它没有做的事——即便确认了东西已经没了,这段代码依然会把那条错误照原样报出来,连同后端返回的原始错误一起。它没有把失败改写成成功。这是个有意思的取舍:宁可留下一个「报了错、可对话框自己关了」的自相矛盾组合,也不替后端下一个「其实成功了」的结论。前端确实不知道那个 Err 背后是不是还藏着别的问题,它只知道这条备份不在列表里了。
怎么判定你遇到的正是这一种
不用去翻磁盘,看两个信号的组合就够了:
- 收到了删除失败的错误提示;
- 同时那个二次确认的状态被清掉了。
只要这两件事同时发生,就说明代码走到了 577-584 行那个分支——而这个分支的进入条件是写死的两条:删除这一步的结果标记为失败,并且刷新回来的列表里已经找不到这条备份。
后半条才是真正的权威依据,它比对的不是提示文案、也不是本地缓存,而是 560-571 行那次强制刷新拿回来的备份列表。所以这两个信号的组合虽然是从提示层看到的,落点却在数据层:它等价于「权威列表里已经没有这条备份了」这一个事实。换句话说,这个组合本身就是「删除报了错,但权威数据确认东西已经没了」的唯一出口,不需要另找证据。
反过来,如果报了错、确认状态还留着,那就落不到这个分支,得往下一节看。
处置与验证
处置其实是「不用处置」:代码已经替你把权威列表拉过一遍了,不要再点一次删除。重复删一个已经不存在的目录,只会再拿到一个错误。
验证的落点是刷新后的备份列表本身,而不是提示文案。这条链路里唯一称得上权威的数据源就是 560-571 行那次 refetch 的返回值,上一节的判定分支用的也正是它,而不是任何缓存或本地状态。
顺带说一下并发这一侧,因为它会影响你「再点一次」的后果。这个函数一进来就有一道守卫:checkUpdatesLockRef 与 writeLockRef 任一为真就直接返回。也就是说,正在检查更新、或者已经有别的写操作在跑的时候,删除请求根本不会发出去。而 onConfirm 内部走的是 beginWrite(allowOpenDialog = true)——那个例外参数用于「确认对话框已经开着、此刻执行的正是它自己的回调」这种场景,否则刚开的对话框会被自己的锁挡住。锁的释放交给 endWrite(),无论成败都会归还。
什么情况说明不是这个原因
以下四种都长得像,都不是。
一、报了错,确认状态还在。 这说明没走进 577-584 行的分支,只有两种可能:刷新回来的列表里这条备份还在(那就是真没删掉),或者刷新这一步自己也失败了、压根没拿到那份权威列表。后一种在控制台会留下一条错误日志(564-571 行的 console.error),这是区分两者的唯一线索。
二、点了删除完全没反应,连错误提示都没有。 那是函数开头那道并发守卫拦下的,跟目录删除没有关系。它拦的是并发,不是失败。
三、你操作的是卸载 Skill,不是删除备份。 两条路径完全不同:卸载走的是 useUninstallSkill,删除备份走的是 useDeleteSkillBackup,本文这段处理只包住后者。另外要留意一个版本差异——v3.19.2 时卸载成功后的提示只有成功一种,v3.20.1 在里面加了一个分支(329-353 行):result.piCleanupIncomplete || Boolean(result.preservedPiPath) 为真时改用 toast.warning。也就是说,卸载路径上「操作完成了但有残留」这件事,v3.20.1 是通过换一种提示级别来表达的,跟备份删除的这套处理不是同一个机制。
四、批量操作里某一条失败。 「全部更新」是前端的串行循环(471-497 行),对每条更新逐个 await,单条失败只弹那一条的错误、继续跑下一条,最后按成功计数汇总一次。它没有本文这套「拉权威列表复核」的逻辑,失败就是失败。
最后把平台这件事说清楚。前端这段代码没有区分平台:Windows 与 macOS、Linux 走的是同一个 handleDeleteBackup,同一套三重处理,判定信号也完全一样。至于底层的目录删除在不同系统上各自会以什么方式失败,源码这一层没有给出依据,本文不作推断。你需要知道的只是:这段前端处理的存在,本身就是「删除可能部分完成」这件事在这个项目里被当真了。
本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册、
路由指南与发布说明,以及 src/、src-tauri/、tests/ 的源码整理,
核对日 2026-08-31,对应仓库快照 3217f725(仓库内版本号 3.20.1)。
本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用,
因此不涉及界面外观、操作手感与切换速度的任何描述。
文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。
该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。
留言讨论
评论发布后会被人工复核,违规内容将被删除。
如果发表没有反应,可以前往联系我们告诉我们。