Skills 装成软链还是复制:两种策略的差别与 skill-backups

2026-08-10

在 CC Switch 里装一个 Skill,它到底是把文件夹复制~/.claude/skills/,还是在那儿放一个软链接指回统一存储?

仓库里能读到两种说法。用户手册 docs/user-manual/zh/3-extensions/3.3-skills.md 第 100 行写的是「安装会将技能文件夹复制到本地」;同一份文档第 232 行又有一条提示,把「软链接 vs 复制」当成一个可配的分发方式说明。而代码里 SyncMethod 的默认值是 Auto,语义是优先 symlink、失败才回退 copy(src-tauri/src/services/skill.rs:25-36)。两处表述不一致,以我们实读的仓库状态为准;至于为什么会这样,不在本文讨论范围。

以下全部基于我们本地 clone 的 cc-switch 仓库快照 c39c903(提交日期 2026-08-10),仓库内版本号 3.19.2。我们只读源码与文档文本,没有安装也没有运行过这个桌面应用,因此不涉及任何界面与操作过程的描述。

先分清「存在哪」和「怎么分发」

Skills 这块有两层存储,混在一起看就会绕晕。

第一层是 SSOT(单一事实源),也就是 skill 内容真正落盘的地方。它有两个可选位置,二选一:CcSwitch 对应 ~/.cc-switch/skills/(默认),Unified 对应 ~/.agents/skills/src-tauri/src/services/skill.rs:38-47509-520)。模块头注释把这套架构写成 v3.10.0+ 的统一管理:安装时下载到 SSOT,按需同步到各应用目录,数据库只存安装记录与启用状态(src-tauri/src/services/skill.rs:1-6)。

第二层才是各个 CLI 自己的 skills 目录,也就是「分发落点」。get_app_skills_dir 给出的默认值是八条:

应用默认 skills 目录
Claude~/.claude/skills
ClaudeDesktop~/.claude-desktop/skills
Codex~/.codex/skills
Gemini~/.gemini/skills
GrokBuild~/.grok/skills
OpenCode~/.config/opencode/skills
OpenClaw~/.openclaw/skills
Hermes<hermes_dir>/skills

src-tauri/src/services/skill.rs:575-584,每一条都可被 settings.json 里的 override 目录覆盖,见 :529-568。)

这张表要和手册对着看:3.3-skills.md:13-21 写「Skills 功能支持五种应用」并列出 Claude / Codex / Gemini / OpenCode / Hermes;而代码这边 GrokBuild 与 OpenClaw 同样返回了目录,真正在同步环节被跳过的只有 ClaudeDesktop —— sync_to_app_dirremove_from_appAppType::ClaudeDesktop 直接返回 Ok(src-tauri/src/services/skill.rs:1698-17001873-1875)。差异陈述到此为止。

所谓「软链还是复制」,说的完全是第二层:SSOT 里那份内容不动,变的只是各应用目录下那个入口是链接还是实体拷贝

Auto 里那个容易被忽略的前置判断

SyncMethod 三个取值:Auto(默认,优先 symlink、失败回退 copy)、SymlinkCopysrc-tauri/src/services/skill.rs:25-36)。

但只看这个枚举,你会以为 Auto 的行为是「先试软链,不行再拷」。实际分支顺序不是这样。Auto 走到的第一个判断是:目标路径已经存在、并且它不是一个 symlink——这种情况直接走复制,函数当场返回,根本不会尝试建软链;只有目标不存在、或者目标本身就是个旧 symlink 时,才会先删旧链再尝试创建 symlink,创建失败才回退复制(src-tauri/src/services/skill.rs:1717-1743)。

这个顺序有个直接后果:Auto 模式下,同一个 skill 在不同应用目录里可能是不同形态。哪个应用目录下原先就躺着一个同名实体目录(比如你以前手工放过、或者曾经在 Copy 模式下同步过一次),那个位置就会继续是复制;干净的位置则会拿到软链。这不是随机的,是上面那个「目标已存在且不是 symlink」的前置判断决定的确定行为(src-tauri/src/services/skill.rs:1717-1743)。

要判断某个落点当前是哪一种,读代码没用,得看文件系统本身。这属于操作系统层面的通用做法、不是该项目文档给的步骤:Linux / macOS 侧 ls -l ~/.claude/skills/ 看条目有没有指向 SSOT 的箭头;Windows 侧在对应目录用 dir 看条目类型标记(目录 symlink 会显示为链接类型而不是普通目录)。这两条都只是读取,不改任何东西。

代码里另有一处按平台分岔,落在删除这一侧:删 symlink 时 unix 走 remove_file,Windows 的目录 symlink 走 remove_dirsrc-tauri/src/services/skill.rs:1770-1776)。这条只说明删除动作在两个平台上调的不是同一个系统调用,不能拿来推断哪个平台更容易建链失败——symlink 在不同平台上的可用性差异我们没有依据,不展开。至于创建这一侧,代码写明的应对只有一条:Auto 下创建 symlink 失败就回退复制,而不是报错中止(:1717-1743)。

顺带一提,老接口 copy_to_app 已经被标了 #[deprecated(note = "请使用 sync_to_app_dir() 代替")]src-tauri/src/services/skill.rs:1764-1767)。你在仓库里搜到它时别再拿它当当前行为的依据。

复制这条路:临时目录 + rename,以及一道拒绝同步的闸

回退到复制时,代码不是直接往目标目录里写。流程是先拷到同一父目录下的一个临时目录,临时名形如 .{sanitized}.tmp-{pid}-{nonce},拷完再 rename 到目标位置(src-tauri/src/services/skill.rs:1799-1830)。中途出错会清掉临时目录。这样做的意义是目标位置要么是旧的完整状态、要么是新的完整状态,不会停在拷了一半的中间态。

同步前还有一道硬闸:源目录必须含 SKILL.md,否则直接报错「Skill 源目录缺少 SKILL.md,拒绝同步以避免覆盖目标目录」(src-tauri/src/services/skill.rs:1786-1797)。这句错误信息里的「以避免覆盖目标目录」说明了它的动机——既然复制路径最终会 rename 覆盖目标位置,源目录是空的或者结构不对时就不能让它跑下去。

装进来的东西:仓库归档、本地 ZIP,与四个上限常量

安装来源有两条。

一条是 GitHub 仓库归档,URL 模板是 https://github.com/{owner}/{name}/archive/refs/heads/{branch}.zip,下载超时 60 秒(src-tauri/src/services/skill.rs:2529-2531687-700);分支候选会追加 mainmaster 逐个尝试,失败时清理上一轮残留(:2520-2542)。出口处有个断言 assert_github_archive_url:scheme 必须 https、host 必须 github.com、path 必须以 /{owner}/{name}/archive/refs/heads/ 开头(:2360-2374)。另有指向 https://skills.sh/api/search 的公共注册表搜索,参数 q / limit / offset,超时 10 秒(:3355-3378)。内置的默认仓库 SkillStore::default() 是 4 个(:133-163);手册 3.3-skills.md:37-41 的表格列的是 3 行。同样只陈述差异。

另一条是本地 ZIP(install_from_zip),注释写的流程是解压到临时目录 → 扫描含 SKILL.md 的目录 → 复制到 SSOT 并入库 → 同步到当前应用(src-tauri/src/services/skill.rs:3045-3056);ZIP 里一个 skill 都没有时报 NO_SKILLS_IN_ZIP:3064-3070)。

解压这一步有四个上限常量,加一个计费系数:MAX_ARCHIVE_ENTRIES = 10_000MAX_ARCHIVE_TOTAL_BYTES = 512 MiBMAX_SYMLINK_TARGET_BYTES = 4 KiBMAX_ARCHIVE_DOWNLOAD_BYTES = 128 MiB,以及 DIRECTORY_BUDGET_COST = 4096src-tauri/src/services/skill.rs:274-287)。为什么要有上限,注释自己写了:归档字节由第三方完全控制(仓库可经 deeplink 添加,且 branch 可把下载落点改写到攻击者自传的 release asset),没有上限时一个几 MB 的压缩炸弹就能塞满磁盘(:269-273)。

这里还有一条和本篇主题正相关的处理:ZIP 里的 symlink 不会被还原成真 symlink,代码改为把目标内容复制过来,注释说明是「以确保跨平台兼容且 skill 内容自包含」(src-tauri/src/services/skill.rs:2955-2960)。也就是说,软链这件事只出现在「SSOT → 应用目录」这一跳,归档内部的链接结构进不来。

skill-backups:卸载不是直接删

备份目录是 ~/.cc-switch/skill-backups/src-tauri/src/services/skill.rs:521-527)。卸载的顺序是先备份再删:从所有应用目录移除 → 从 SSOT 删除 → 删数据库行(:825-840)。

备份目录命名是 {YYYYmmdd_HHMMSS}_{slug},同秒冲突时追加 _{counter};每份备份内部是一个 skill/ 子目录加一个 meta.json:2909-2938)。保留数量由常量 SKILL_BACKUP_RETAIN_COUNT = 20 决定,超出后按 mtime 从旧到新删除(:267:2866-2876)。注意这是代码里的默认配置,不是「你一定会有 20 份可用备份」的保证。代码里就有一条明确的反例:卸载时若记录里的 directory 字段非法,代码会跳过全部文件系统操作(:815-824),这一趟自然也不会产生这份备份——这条兜底下面单独讲。

恢复侧有两道校验。一是 backup_path_for_id 拒绝含 ../\ 或空白的 backup_id(:2880-2890);二是恢复要求备份内的 skill/SKILL.md 存在,否则报 “Skill backup is invalid or missing SKILL.md”(:1385-1391)。

还有一处兜底值得单独说:卸载时如果记录里的 directory 字段非法,代码会跳过全部文件系统操作、但仍然删掉数据库行。注释解释了原因——否则「用户就再也无法从界面删掉这条记录,只能手改 SQLite」(:815-824841-848)。这是一个「宁可留下孤儿目录,也别让记录卡死」的取舍,你要清楚它的后果:这种情况下磁盘上可能还留着文件,需要自己去 SSOT 目录确认。

更新检测那侧用的是 SHA-256 目录内容哈希:递归遍历非隐藏文件,按相对路径字典序,逐文件 feed "相对路径\0内容\0":868-873)。知道这个口径你就能推断出,改了隐藏文件不会体现在这个哈希里。

你可以自己复现的核查路径

想把上面这些落到你自己的仓库副本上,四步就够:

  1. 打开 src-tauri/src/services/skill.rs,先看 :25-36SyncMethod 定义,再跳到 :1717-1743Auto 分支的判断顺序——注释和分支顺序是本篇的核心对照点。
  2. docs/user-manual/zh/3-extensions/3.3-skills.md:100:232 两处并排看,差异就在那两行。
  3. 常量都在 :267:274-287grep -n "RETAIN_COUNT\|MAX_ARCHIVE" src-tauri/src/services/skill.rs 一条命令全能捞出来。
  4. 想知道这块代码的测试覆盖到哪,grep -c "#\[test\]" src-tauri/src/services/skill.rs 得到 39;这个文件本身 4728 行(wc -l)。测试数量是静态计数,我们没有跑过构建或测试,也不代表这些测试当前是通过的。

最后一句留给边界:这套机制读写的是 ~/.claude~/.codex 这类真实 CLI 配置目录,SSOT 与备份目录也都在你本机的用户目录下,属于敏感数据。软链和复制的差别只是分发形态,不构成任何隔离或防护,也别把「有 skill-backups 兜底」理解成删了都能找回来——保留 20 份是上限策略,不是承诺。


本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册与发布说明、src/config/ 的预设定义与 src-tauri/src/ 的后端源码整理,核对日 2026-08-10,对应仓库快照 c39c903。本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用,因此不涉及界面外观、操作手感与切换速度的任何描述。文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。安全相关做法请结合自身环境评估,本文不构成安全方案建议。

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