CC Switch 向系统要了哪些权限:capability 白名单逐条对账
一个功能失效最难查的形态,不是抛异常,也不是白屏,而是”什么都没发生”。代码在,调用写了,日志干净,控制台没红字,就是不生效。CC Switch 在 v3.20.0 上就有这么一处:按官方发布说明的记述,“数据库版本过新”的恢复界面里那个退出按钮不起作用,配置加载失败之后应用也没有按设计退出、而是径直进入了正常界面。根因不在业务代码里,在一个 JSON 数组少了一行字符串。
这篇就从那一行讲起,顺带把 src-tauri/capabilities/default.json 里的权限逐条对到调用点上——这份文件是理解 Tauri 应用”能对本机做什么”的第一张地图,也是最容易被当成模板抄一遍就再也不看的文件。
先交代这篇的依据
本文只做静态源码阅读,对象是 cc-switch 仓库 v3.20.1 的快照(commit 3217f725),核对日 2026-08-31,另外拉了 v3.19.2 的快照(c39c9032)做版本对照。我们没有安装、没有编译、也没有运行过这个桌面应用,所以下文不会出现任何关于界面长什么样、按钮在哪里、点下去多快响应的描述。凡涉及”用户看到的现象”,都来自仓库里 docs/release-notes/ 的官方发布说明或提交信息原文,我会逐处标明出处。文中的行号可以直接拿去仓库里对,对不上就说明你手上的版本不是这个快照。
capability 约束的到底是谁
find src-tauri/capabilities -type f 的结果只有一个文件:src-tauri/capabilities/default.json。这份文件不长,结构一眼看得完:
:2的$schema指向../gen/schemas/desktop-schema.json,而src-tauri/.gitignore:4里有/gen/schemas——这个 schema 不在仓库里,是构建过程生成的。所以你 clone 下来直接看这个文件,编辑器里的字段补全大概率是没有的。:3"identifier": "default",:4的 description 就一句enables the default permissions。:5"windows": ["main"]——权限只作用在 label 为main的窗口上。而src-tauri/tauri.conf.json:15定义的正是"label": "main",整个应用只声明了这一个窗口,两处刚好闭合。:6往下是permissions数组。
关键的机制在这里:capability 白名单管的是”前端 webview 通过 IPC 能调用什么”,不是”这个进程能对系统做什么”。 Rust 侧自己调 Tauri API,完全不受 capability 约束。这一条如果没绕清楚,下面两个反直觉的现象就都解释不了。
逐条对账表
下表按 v3.20.1 快照里 permissions 数组的原始顺序抄下来,右列是我们在源码里找到的对接点。数组的条目会随版本增减,这是某一个时间点的状态,不是这个项目的固定形态。
| 权限条目 | 对应的调用点 |
|---|---|
core:default | Tauri 上游定义的核心默认集,仓库里没有展开,具体包含哪些子权限我们没有核实 |
opener:default | 前端零处引用(见下一节) |
updater:default | src/lib/updater.ts:31 动态 import("@tauri-apps/plugin-updater") 之后调 check() |
log:default | src/lib/frontendLogger.ts:1 从 @tauri-apps/plugin-log 引入 error |
core:window:allow-set-skip-taskbar | 前端零调用,实际调用点全在 Rust 侧(见下一节) |
core:window:allow-start-dragging | 自绘标题栏拖拽,标记点在 src/App.tsx:1188 与 src/components/common/FullScreenPanel.tsx:135 的 data-tauri-drag-region |
core:window:allow-minimize | src/App.tsx:975 |
core:window:allow-toggle-maximize | src/App.tsx:985 |
core:window:allow-is-maximized | src/App.tsx:509 与 :986,前者在 resize 监听里同步窗口状态 |
core:window:allow-close | src/App.tsx:995 |
core:window:allow-set-decorations | src/App.tsx:537 的 setDecorations(!useAppWindowControls) |
process:allow-exit | src/main.tsx:75 与 src/components/DatabaseUpgrade.tsx:292 |
process:allow-restart | 后端侧走 src-tauri/src/commands/settings.rs:187 的 app.restart() |
dialog:default | src/main.tsx:15 引入 message,调用点在 :60 |
这张表最值得读的不是”有哪些”,而是中间那一段 core:window: 开头的细粒度权限为什么会存在。它们对应的是一件很具体的事:这个应用支持在系统标题栏和应用自绘窗口控件之间切换,自绘那一套就得自己实现拖动、最小化、最大化切换、关闭。每一个动作都要单独授权,少一条就少一个能用的按钮。src/index.css:116 与 :121 给 data-tauri-drag-region 和 .no-drag 各自定了样式,src/lib/platform.ts:38 的注释还写明这个属性是 wry 侧的存在性检测——拖拽这件事从 CSS 到 IPC 权限跨了三层。
Windows 侧要单独说一句:src-tauri/tauri.windows.conf.json 会覆盖主配置的窗口段,:7 把 title 从空字符串改成 CC Switch,:8 把 titleBarStyle 从 Overlay 改成 Visible。也就是说同一套 window 权限,在 Windows 和 macOS 上服务的是不同的标题栏形态,但权限清单本身是共用的一份。
那一行 process:allow-exit
把 v3.19.2 与 v3.20.1 的 default.json 直接 diff,输出只有一行新增:+ "process:allow-exit"。v3.19.2 时数组里只有 process:allow-restart,v3.20.1 才补上了 process:allow-exit。
对应提交是 4549d290,标题 fix(capabilities): grant process:allow-exit so exit buttons can quit the app,提交正文说得很清楚:数据库版本过新的恢复界面和配置加载失败这两条路径都调了 @tauri-apps/plugin-process 的 exit(),但默认 capability 只给了 process:allow-restart,于是 IPC 调用被拒绝,而拒绝的结果被 void / await 静默吞掉了。落到文件上,这处 capability 的改动只有一行插入。
两个调用点都在前端:
src/main.tsx:75的await exit(1),走的是配置加载失败之后的强制退出路径;src/components/DatabaseUpgrade.tsx:292的onClick={() => void exit(0)},是恢复界面里的退出动作。
注意这两处写法的差别——一个 await,一个 void。这正是”静默”的技术来源:void 直接把返回的 Promise 丢掉,rejection 无人接手;await 那一处则会把异常抛进它所在的异步函数里,而那个函数已经处在错误处理的末端。两种写法在正常路径下都没问题,恰恰在权限被拒这种非预期失败上一起失了声。
中文发布说明 docs/release-notes/v3.20.1-zh.md:116-118 对同一件事的记述更贴近用户视角,标题是”恢复界面的退出按钮真的能退出了”,并注明该问题由外部贡献者更早独立发现并提交过修复。
值得停下来想一下的是:这个 bug 具备了所有”难查”的特征——调用代码存在、按钮组件存在、没有报错、没有崩溃、日志里也不会有醒目的痕迹。它只在两个低频的异常界面里才会露头,而这两个界面本身就是给出事时用的。要在这类路径上尽早发现问题,靠的不是读业务代码,而是把权限清单当成一份需要对账的清单来读。这条思路和这个项目在启动期的自救设计是同一套思维,关于启动序列我们另有一篇专门讲,可以对照 CC Switch 启动时干了什么。
反过来:授了权,前端却零调用
清单里有两条,权限给了,前端一次都没用。
opener:default。 在 v3.20.1 快照的 package.json 里通读 dependencies,@tauri-apps/plugin-* 只有 dialog、log、process、updater 这几个,没有 opener 的 JS 包(这份依赖清单会随版本增减,重点不是它当下有几个,而是 opener 不在其中)。在 src 目录里 grep opener,命中的全是 HTML 里的 rel="noopener noreferrer",跟这个插件没有半点关系。真正”打开外部链接 / 打开文件管理器”的动作全在 Rust 侧:src-tauri/src/commands/config.rs:197 与 :250、commands/hermes.rs:127、commands/misc.rs:30 与 :57、commands/workspace.rs:357、src-tauri/src/tray.rs:1024,插件注册在 src-tauri/src/lib.rs:443。
core:window:allow-set-skip-taskbar。 同样的形态。在 src 下 grep setSkipTaskbar 是零命中,调用点全在 Rust:src-tauri/src/lib.rs:429、:1345、:1788,src-tauri/src/lightweight.rs:11、:46、:84,以及 src-tauri/src/tray.rs:1004。
回到前面那条机制:Rust 侧调 API 不需要 capability,所以这两条权限对当前的功能来说是多余的。至于是历史遗留、还是给未来的前端实现预留的口子,我们没有找到对应的提交说明,不作推断。能确定的只有一件事:清单和实际调用之间在这两条上对不齐。
再反过来:注册了插件,清单里却没有
grep -n "\.plugin(" src-tauri/src/lib.rs 能看到一批插件注册,其中这几个在 capabilities 里根本不出现:tauri_plugin_single_instance(lib.rs:351)、tauri_plugin_deep_link(:408)、tauri_plugin_store(:444)、tauri_plugin_window_state(:446-449)。
这不是漏配。前四个前端从不直接调,package.json 里也没有它们对应的 JS 包,因此压根不需要 capability——这是”Rust-only 插件”的典型形态。同样在 setup 内动态注册的 tauri_plugin_log(:468)和 tauri_plugin_updater(:510,带 #[cfg(desktop)])就不一样,它们前端要用,清单里也确实各有一条。
updater 那处的注册还包在错误处理里(src-tauri/src/lib.rs:508-514),注释写的是:若配置不完整(比如缺少 pubkey),跳过 Updater 而不中断应用,失败时打一条 log::warn!。
顺带一提,README_ZH.md:534 的后端技术栈清单只列了 updater / process / dialog / store / log 五个插件,而 src-tauri/Cargo.toml:31-38 实际还有 tauri-plugin-opener、tauri-plugin-deep-link、tauri-plugin-window-state,:87 另有 tauri-plugin-single-instance。两处对不上,以我们实读的 Cargo.toml 为准。说完差异就停,不延伸。
你自己怎么回查这份清单
如果你要在自己的 Tauri 项目里做同类对账,可以照着这个顺序走:
# 1. 清单里有哪几条
find src-tauri/capabilities -type f
# 2. 某一条权限前端到底调没调
grep -rn "setSkipTaskbar" src | wc -l
# 3. 跨版本比对,看这一版动了哪几条
diff -u <旧快照>/src-tauri/capabilities/default.json \
<新快照>/src-tauri/capabilities/default.json
以上是按仓库中已有的命令语义组合出来的示例,未经实测,具体请以你本机工具的实际输出为准。第二步是整套动作里最有价值的一步:对每一条权限都问一句”前端哪一行在用它”,答案只有三种——找得到调用点、找不到但 Rust 在用、两边都找不到。第三种就该问一下这条还留着做什么了。
反过来的对账同样要做:先在前端 grep 出所有 IPC 调用,再回头看清单里有没有对应授权。process:allow-exit 那个 bug 就是从这个方向能提前抓到的——exit( 在前端有两处调用,清单里没有那条权限,对账当场就露馅。
边界
最后框清楚这篇能得出什么、不能得出什么。
能得出的:截至 v3.20.1 快照,这份 capability 清单的每一条对应哪个调用点;哪两条前端零调用;哪几个插件因为是 Rust-only 而不需要出现在清单里;process:allow-exit 是这一版相对上一版唯一的权限变化。
不能得出的:这份清单不代表这个应用对你机器的完整影响面。capability 只管前端经 IPC 的调用,Rust 侧读写文件、起网络请求、操作托盘都不在它的管辖内。这个工具本身就要读写 ~/.cc-switch、~/.claude、~/.codex 这类真实的 CLI 配置目录,还会在本机保存 API Key,那些都是本机敏感数据,跟 capability 清单是两回事,别把这份清单当成安全评估的结论。关于这个工具管的是哪一层、数据落在哪里,可以看 CC Switch 是什么。
也不能得出”清单短就更安全”这类判断。清单条目是按功能需要一条条加的,加得多不等于风险大,加得少也不等于收敛好——像上面那条被漏掉的 exit,短的代价是功能静默失效。
本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册、路由指南与发布说明,以及 src/、src-tauri/、tests/ 的源码整理,核对日 2026-08-31,对应仓库快照 3217f725(仓库内版本号 3.20.1)。本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用,因此不涉及界面外观、操作手感与切换速度的任何描述。文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。安全相关做法请结合自身环境评估,本文不构成安全方案建议。
留言讨论
评论发布后会被人工复核,违规内容将被删除。
如果发表没有反应,可以前往联系我们告诉我们。