`ccswitch://` 一键导入:四类载荷与解析器的边界
ccswitch:// 是 CC Switch 注册的自定义协议,用来把一份供应商配置、一段提示词、一组 MCP 服务器或一个 Skill 仓库坐标从链接里导进本机。听上去像”一个协议四种用法”,但真去读 src-tauri/src/deeplink/parser.rs 会发现:骨架是共用的,四类载荷的参数契约却完全不对称——同一个”指定应用”的语义,在四类里有三种不同写法,其中一类干脆不让你指定。
以下全部基于我们本地 clone 的 cc-switch 仓库快照 c39c903(提交日期 2026-08-10),仓库内版本号 3.19.2。我们只读源码文本与 docs/ 下的用户手册,没有安装也没有运行过这个桌面应用,所以本文只讲解析器的代码语义,不涉及任何界面与实际运行表现。
骨架:四道校验,缺一不可
parse_deeplink_url 在分发到具体解析器之前,先连着做四件事(src-tauri/src/deeplink/parser.rs:20-56):
- scheme 必须是
ccswitch,其它 scheme 直接判失败; - 版本取自 URL 的 host 段,必须恰好等于
v1,不是v1就过不去; - path 必须恰好等于
/import; - query 里必须有
resource,缺了直接失败。
第二条最容易被误读。ccswitch://v1/import?... 里的 v1 不是路径的一部分,它在 URL 语法里占的是 host 的位置,所以你把它写成 ccswitch:///v1/import 或者 ccswitch://import/... 都过不去——前者 host 为空,后者 host 是 import 而不是 v1。仓库里有两个测试正对着这两道校验:test_parse_invalid_scheme(src-tauri/src/deeplink/tests.rs:124)与 test_parse_unsupported_version(tests.rs:133)。
第四步之后是一个 match,resource 只接受 provider / prompt / mcp / skill 四个值,其余一律 “Unsupported resource type”(parser.rs:59-66)。四类载荷从这里开始各走各的。
反直觉的那一处:三种「指定应用」的写法
如果你把这四类当成”同一张表填不同的字段”,第一个坑就在这里。“这条链接要导给哪个 CLI”这件事,四类载荷用了三种完全不同的表达方式:
| resource | 指定应用的参数 | 取值范围 | 依据 |
|---|---|---|---|
provider | app(必填) | 7 项:claude、codex、gemini、grokbuild、opencode、openclaw、hermes | parser.rs:82-89 |
prompt | app(必填) | 同上 7 项 | parser.rs:191-198 |
mcp | apps(必填,复数) | 7 个可选值,额外接受别名 grok(等价 grokbuild) | parser.rs:261-273 |
skill | 无此参数 | 解析结果里 app 被硬编码为 "claude" | parser.rs:349 |
这张表的读法是:前两行的参数名是单数 app,第三行是复数 apps,第四行根本不接受。skill 那一行的源码注释写得很直白:“Skills are Claude-only”。也就是说,你手搓一条 skill 深链再挂个 app=codex 上去,这个参数不会报错,它只是被忽略掉——最终落库的 app 是 claude。
mcp 那个别名也值得单独记一笔:只有 apps 这一处额外认 grok 这个写法,provider 与 prompt 的白名单里没有它。同一个应用在同一份解析器的不同分支里,一处认两种拼法、两处只认一种。这类差异我们只陈述,不推断原因。
顺带说清各类的必填项,省得你对着报错猜:provider 必填 app 与 name,homepage / endpoint / apiKey 可选,注释注明可选项是为 v3.8+ 的配置文件自动填充准备的(parser.rs:76-99);prompt 必填 app、name、content(parser.rs:185-210);mcp 必填 apps 与 config(parser.rs:255-293);skill 只必填 repo,directory 与 branch 可选(parser.rs:327-340)。
repo 的格式校验也比”含斜杠即可”严一档:必须是恰好两段的 owner/name,a/b/c 这种三段写法会在格式校验这一步被挡掉(parser.rs:327-340)。
endpoint 可以是多个,config 必须先 Base64
第二处容易踩空的是编码约定。
endpoint 支持逗号分隔的多个 URL,解析时逐个过 validate_url,报错信息会标成 endpoint[i] 的形式告诉你是第几个坏了(parser.rs:107-115)。validate_url 本身只放行 http 与 https 两种 scheme(src-tauri/src/deeplink/utils.rs:10-22)。这三种写法各有对应测试:test_parse_multiple_endpoints_comma_separated、test_parse_single_endpoint_backward_compatible、test_parse_endpoints_with_spaces_trimmed(tests.rs:942、956、969),最后一个说明逗号两侧的空格会被 trim 掉。
编码这一层的关键在两个参数上:
mcp的config:先 Base64 解码 → 转 UTF-8 → 解析 JSON → 解出来的对象必须含mcpServers键,为空则报 “No MCP servers found in config”(src-tauri/src/deeplink/mcp.rs:62-89)。另外config_format在解析器里被硬编码成"json",注释是 “MCP config is always JSON”(parser.rs:255-293),你传别的值不起作用。prompt的content:走decode_base64_param,结构体字段注释写明是 “Base64 encoded Markdown content”(src-tauri/src/deeplink/prompt.rs:47、src-tauri/src/deeplink/mod.rs:83-85)。
Base64 这一步有一层容错,写在 utils.rs:24-74:解码前会尝试把空格还原成 +(URL 传输中 + 常被当成空格)、补齐 = padding,然后在 STANDARD / STANDARD_NO_PAD / URL_SAFE / URL_SAFE_NO_PAD 四种引擎之间轮询。对应测试是 test_import_prompt_allows_space_in_base64_content(tests.rs:822)。这层容错针对的就是 + 被转成空格、padding 被截掉这两种情形(utils.rs:24-74)。
还有一个小机制:infer_homepage_from_endpoint 会把 host 的 api. 或 api- 前缀去掉再拼成 https://{host},用于链接没带 homepage 时的补齐(utils.rs:88-98),测试见 test_infer_homepage 与 test_infer_homepage_from_endpoint_without_homepage(tests.rs:178、979)。
一条链接可以带脚本,但带上不等于开启
provider 类深链可以携带用量查询脚本。默认行为写在 src-tauri/src/deeplink/mod.rs:117-120 的字段注释里,原文是:carrying a script is not itself a decision to run it; the link must say usageEnabled=true——usage_enabled 默认禁用,链接里必须显式写 usageEnabled=true 才会启用。
这条约定在 tests.rs 里有 5 个专门的测试守着(tests.rs:297、382、406、427、475),从函数名就能看出各自守什么:不把 provider 的凭据复制给用量脚本、不因为携带了代码就视为启用、显式请求时才照办、与 provider 相同的凭据会被省略、不同的用量凭据要保留。另有一个非测试的构造辅助函数 usage_script_request(tests.rs:346)。
这是本篇里唯一一处涉及”链接会不会让本机跑第三方代码”的设计,值得读者自己去 mod.rs:117-120 看一眼原文。同时必须说清楚:深链导入会把 API Key 一类内容写进本机配置,这是本机敏感数据,源码里的默认值只是默认配置,不构成任何”这样就安全”的保证。
用户手册与代码的四处口径差
docs/user-manual/zh/5-faq/5.3-deeplink.md 与 3.2-prompts.md 里关于深链的描述,有四处与代码对不上。按纪律,我们只标出每一处在文档与代码里的具体位置,不推断哪一处”才算数”、也不据此评价项目:
- prompt 深链的整体格式。
3.2-prompts.md:152-154写的是ccswitch://import/prompt?data=<base64编码的预设>;代码要求 host 必须为v1、path 必须为/import,参数是resource=prompt&app=&name=&content=,没有data这个参数(parser.rs:28-47、180-213)。 mcp的config编码。5.3-deeplink.md:78-83、:110把config描述为”MCP 服务器配置(JSON 格式)“,示例给的是 URL 编码的裸 JSON;代码要求先 Base64 解码,且解出的 JSON 必须含mcpServers对象,否则报 “MCP config must contain ‘mcpServers’ object”(deeplink/mcp.rs:68-83)。prompt的content编码。5.3-deeplink.md:70-74、:116只写”提示词内容”,示例是 URL 编码明文;代码走decode_base64_param(deeplink/prompt.rs:47)。app白名单的长度。5.3-deeplink.md:41的通用参数表列 5 项(claude / codex / gemini / opencode / openclaw);代码接受 7 项,多出 grokbuild 与 hermes(parser.rs:82-89)。
说完就停。要用哪一份口径,以你自己 clone 到的仓库状态为准。
你可以怎么自己复核
这几条都能在本机分钟级验完,不需要装这个应用:
# 1. 看骨架四道校验
sed -n '20,66p' src-tauri/src/deeplink/parser.rs
# 2. 数 deeplink 模块的测试量
grep -c "#\[test\]" src-tauri/src/deeplink/tests.rs
# 3. 把手册那四处口径与代码并排看
grep -n "ccswitch://" docs/user-manual/zh/5-faq/5.3-deeplink.md
第二条的结果是 33,tests.rs 本身 989 行;整个 src-tauri/src/deeplink/ 目录 8 个文件合计 3161 行,其中 provider.rs 一个就占 1182 行,skill.rs 只有 64 行(wc -l src-tauri/src/deeplink/*.rs,2026-08-10 采集)。测试文件占了模块近三分之一的行数,这个比例本身是个可核查的事实;但要注意我们只做了静态计数,没有运行过任何测试,不知道它们当前是否全绿。
另外 deeplink/mcp.rs 里还有一个单测 enabled_apps_merge_covers_every_supported_mcp_client(deeplink/mcp.rs:205-227),断言合并之后 claude / codex / gemini / grokbuild / opencode / hermes 六项全为 true——注意这里是 6 项,与前面 apps 参数白名单的 7 个可选值不是同一个集合。
导进来之后落到哪,以及一处未完成标记
四类载荷的落点也不一样,这决定了”导入成功”到底意味着什么:
- mcp:已存在的服务器只合并 apps 标志,其余字段保持原样;新建的服务器会打上
tags: ["imported"](deeplink/mcp.rs:99-128)。所以对已有条目做深链导入,不会覆盖你手动改过的命令与参数。 - prompt:id 形如
{sanitized_name}-{timestamp_ms},name 只保留字母数字与-/_并转小写;先以enabled: false落库,再按enabled参数决定要不要调enable_prompt(deeplink/prompt.rs:47-79)。 - skill:这一类的落点最轻——只写
skill_repos表,保存仓库坐标而已,branch缺省main,enabled缺省 true(deeplink/skill.rs:36-59)。写入前会先调SkillService::validate_repo_ref做一道纵深拦截,注释解释这样做是为了”让用户当场看到错误而不是让脏数据沉淀进skill_repos表”(deeplink/skill.rs:38-48)。 - provider:
deeplink/provider.rs是四类里最长的一个文件(1182 行),我们这次只读到了mod.rs里的字段定义和其中一行未完成标记:// Fetch remote config (TODO: implement remote fetching in next phase)(provider.rs:608)。这条远程拉取配置的路径在我们读到的快照里还带着 TODO,照实标出来。provider 导入与parse_and_merge_config的合并规则我们没有通读,因此本文不写。
把这几条并起来看,ccswitch:// 的边界其实相当克制:它负责的是”把一份配置的坐标搬进来”,而不是”替你把东西装好跑起来”——skill 只落一行仓库坐标,prompt 默认不启用,用量脚本必须显式开启。你要判断一条来路不明的深链会做什么,从 resource 值开始,照上面四组落点逐条对,就能知道它最多能改动哪张表。
本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册与发布说明、
src/config/ 的预设定义与 src-tauri/src/ 的后端源码整理,核对日 2026-08-10,对应仓库快照 c39c903。
本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用,
因此不涉及界面外观、操作手感与切换速度的任何描述。
文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。
该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。