CC Switch 的 Pi 原生契约与实现边界:models.json、提示词与会话管理的可验证集成指南
CC Switch 对 Pi Agent 的集成遵循"最小契约 + 明确边界"原则:只管理 models.json 中显式存在的供应商节点,只读全局默认项,绝不触碰 auth.json 与登录态。本文档即该契约的权威说明——它既不是 Pi 配置格式的完整镜像,也不承诺代理、OAuth 或全部兼容字段,实现与测试只覆盖前端真正提供的能力。
验证原则贯穿全文:开发和验收均使用当时最新发布的 Pi,CC Switch 不绑定任何特定 Pi 版本。供应商字段、继承顺序和配置值解析,以开发时最新 Pi 的源码入口和真实 CLI 行为为准;资源与会话行为则通过 Pi 的 DefaultResourceLoader、loadSkills、loadPromptTemplates 和 SessionManager 实际运行确认。仓库只保留产品真正消费的最小契约和普通测试,不维护上游源码快照、哈希或版本证据库。
当前消费的契约全景
下表是 CC Switch 与 Pi 原生资源之间的完整契约矩阵,逐条对应产品实际行为与状态来源:
| 资源 | CC Switch 行为 | 状态来源 |
|---|---|---|
models.json |
管理 providers 中的全部显式供应商节点;精确新增、替换和移除 |
文件中的实际条目 |
全局 settings.json |
只读 defaultProvider、defaultModel、sessionDir |
Pi 原生设置 |
auth.json |
不读、不写、不刷新 | Pi /login |
AGENTS.md |
提示库中与文件内容精确匹配的项视为正在使用 | 文件存在及内容 |
SYSTEM.md、APPEND_SYSTEM.md |
直接编辑固定原生文件;不存在即未配置 | 文件存在 |
prompts/*.md |
管理顶层斜杠命令模板;空模板是有效原生文件 | 文件存在 |
skills/<目录> |
目录存在即被 Pi 发现 | 原生 Skills 目录 |
| Sessions JSONL | 读取 Pi 的会话头、树分支、消息和会话名称 | 原生会话文件 |
从后端实现看,这一契约由 src-tauri/src/pi_config/mod.rs 的注释明确声明:"Pi owns account login and the active provider/model in settings.json. CC Switch only manages explicit provider entries in models.json."(Pi 拥有账号登录以及 settings.json 中的活动供应商与模型,CC Switch 只管理 models.json 中的显式供应商条目)。这是理解整个集成设计的纲领性句子。
供应商:只管理显式节点
表单只验证并编辑常用字段
Pi 的结构化表单只验证和编辑以下两类字段,其余原样保留:
- 供应商级:
name、baseUrl、apiKey、api、headers - 模型级:
id、name、reasoning、input、contextWindow、maxTokens
供应商是否"可管理"只取决于一个条件:节点是否显式存在于 models.json.providers 中。这意味着 anthropic、openai、deepseek 等 Pi 内置 ID,乃至携带未知字段的未来节点,都一律按普通显式配置同步。这一点在 pi_config/mod.rs 的测试中得到了明确固化:validate_provider_node("anthropic", &json!({})) 被断言为合法——即使它是 Pi 的内置 key,只要显式出现就可以被管理;而空 key 与非法值类型会被拒绝。
// 摘自 src-tauri/src/pi_config/mod.rs(简化示意)
pub(crate) fn validate_provider_node(provider_key: &str, config: &Value) -> Result<(), AppError> {
if provider_key.trim().is_empty() {
return Err(AppError::InvalidInput("Pi provider key cannot be empty".to_string()));
}
config.as_object().ok_or_else(|| {
AppError::InvalidInput("Pi provider configuration must be an object".to_string())
})?;
Ok(())
}
同文件的 provider_node_accepts_unknown_native_fields 测试(pi_config/mod.rs)证实:即使节点里带有 sdkOption、模型级 compat 等 CC Switch 不认识的字段,也会被当作"可持久化的合法节点"接受。这正是"已有配置中的其他字段原样保留"的代码级保证。
不做的三件事
- 不把 Pi 运行时合并出的内置模型复制回配置:Pi 通过继承/合并机制暴露的内置模型不会写回
models.json,CC Switch 只同步显式书写的节点。 - 不解析或执行
apiKey、Header 中的环境变量与命令表达式:这类字符串原样存储,展开交给 Pi 运行时自行处理,请求也始终由 Pi 自己发出。 - 不写入默认项:启用供应商只把条目加入
models.json,绝不会触碰defaultProvider或defaultModel。
默认供应商与模型的关系
Pi 在全局设置中保存的当前供应商和模型不进入 CC Switch 的供应商列表状态。移除或删除全局默认供应商时,前端只在原有确认框内给出"非阻塞提醒",后端允许继续操作且不改写默认项;编辑显式供应商同样不修改当前供应商或模型——失效引用和回退完全由 Pi 原生处理。
全局设置 vs 项目级设置
需要特别区分两个层级:
- 项目级
.pi/settings.json会在启动 Pi 时按工作目录覆盖全局默认项。由于 CC Switch 的供应商页没有项目上下文,它不扫描项目目录、不猜测活动会话,条件提醒只读取全局默认供应商;项目级默认项继续由对应 Pi 会话和/model命令管理。 - 全局默认项只读。前端状态服务在 src-tauri/src/services/pi_state.rs 中通过
PiStateService::current聚合两路信息:enabled_provider_ids(来自read_pi_native_providers(),即models.json的全部显式 key)与default_provider_id(来自read_pi_native_defaults(),即全局settings.json的defaultProvider,用于 UI 条件提醒)。
其中 pi_config/mod.rs 的 read_pi_native_defaults 只提取三个字段:defaultProvider、defaultModel、sessionDir,且要求 settings.json 根节点必须是对象、目标字段必须是字符串,否则报配置错误。值得注意的是,pi_state.rs 的测试 invalid_global_settings_do_not_hide_provider_membership 证明:即使全局 settings.json 损坏(例如内容为 []),也不会影响 models.json 供应商成员身份的读取——坏设置只会让默认供应商提醒静默失效,绝不拖垮主流程。
Pi agent 目录的解析顺序
所有原生文件都位于"Pi agent 目录"下。后端按如下优先级解析该目录(pi_config/mod.rs):
- CC Switch 自身设置中的 Pi override 目录(
settings::get_pi_override_dir(),来源标记为 "Pi settings override"); - 环境变量
PI_CODING_AGENT_DIR; - 默认路径:
~/.pi/agent(get_home_dir().join(".pi").join("agent"))。
测试 settings_directory_precedes_the_environment 和 relative_agent_directory_is_rejected(pi_config/mod.rs)分别验证了"设置优先于环境变量"和"相对目录必须被拒绝"两条规则。models.json、settings.json、AGENTS.md、SYSTEM.md、APPEND_SYSTEM.md、prompts/*.md 都基于该根目录定位。
用量查询脚本属于元数据
用量查询脚本属于 CC Switch 自身元数据(对应 src-tauri/src/commands/pi.rs 中的 update_pi_provider_usage_script),只更新本地数据库,不重写 models.json——用量统计与 Pi 原生配置始终解耦。
并发与外部修改:成员身份、revision 与原子写
供应商启用 = 成员身份
供应商标识就是成员身份:models.json.providers 中存在该标识即视为"已启用"。CC Switch 在每次进入列表以及应用启动时同步全部显式节点,不按内置 ID、认证字段或模型完整度过滤。状态测试 pi_state.rs 构造了四种节点——普通显式配置、带 oauth 字段的节点、空对象的内置 ID anthropic、仅含 futureField 的未知节点——并断言它们全部出现在 enabled_provider_ids 中。
同步规则可以概括为三条:
- 外部修改同步:原生配置被修改后,用原生内容更新同 ID 的已保存档案(覆盖 CC Switch 数据库中的卡片和完整配置);
- 外部删除降级为停用:原生节点被删除,只改变启用状态(不再视为已启用);
- 移除前保留档案:移除操作前会保存目标节点的最新内容,因此数据库中的卡片和完整配置继续保留,可随时重新启用。
写路径的并发防线
models.json、系统提示文件与模板文件均采用原子写入,并配有三层保护:
- 进程内串行化:所有写操作通过静态
Mutex(MODELS_FILE_LOCK、PROMPT_FILE_LOCK)串行执行,避免同进程并发读改写互相覆盖。 - revision 比较:同一"读-改-写"周期内的跨进程变更,通过 SHA-256 revision 识别。写入前会重新计算文件当前 revision,与读取时保存的期望值比对,不一致即返回
AppError::Conflict。相关实现见 pi_config/mod.rs 与 pi_prompt_files.rs 的ensure_revision。 - 精准替换:列表刷新先把外部修改同步到数据库;保存时只替换目标 provider 节点,启用/移除只增删目标节点,其他 provider 与顶层字段保持不变——绝不整文件覆盖写。
stale_models_revision_does_not_overwrite_an_external_edit 测试(pi_config/mod.rs)还原了典型场景:读取时拿到 revision A,随后 Pi 或其他编辑器写入新内容(revision B),此时 CC Switch 携带过期的 A 去写——写入被拒绝,文件保持为外部的 B 内容。这保证了"绝不覆盖他人编辑"。
对 AGENTS.md,pi_prompt_files.rs 提供了 PiAgentsFileGuard:其守卫跨越"读取 + 数据库更新 + 原子替换"的完整生命周期,调用方可以拿"替换前的文件 revision"与数据库写回做对照,若期间文件被外部改动,就回滚数据库写入。所有原生文件还有统一的 1 MiB 读取上限(MAX_PI_FILE_BYTES / MAX_PROMPT_FILE_BYTES),超限文件在加载前即被拒绝。
不再维护的东西也值得明确:编辑快照和额外的 ownership 状态已被移除——所有者身份完全由 models.json 的实际内容推导,不依赖任何旁路记录。
指令文件与提示词库
Pi 的系统提示与斜杠命令通过固定文件组织,CC Switch 对这些文件的处理非常直接:
| 文件 | 定位 | 语义 |
|---|---|---|
AGENTS.md |
agent 目录根 | 提示库中与该文件内容精确匹配的项视为"正在使用" |
SYSTEM.md |
agent 目录根 | 固定系统提示覆盖文件,直接编辑;不存在即未配置 |
APPEND_SYSTEM.md |
agent 目录根 | 系统提示追加文件,直接编辑;不存在即未配置 |
prompts/<slug>.md |
agent 目录/prompts | 顶层斜杠命令模板,空模板是合法原生文件 |
SYSTEM / APPEND_SYSTEM 的约束
值得注意的细节是:SYSTEM.md 与 APPEND_SYSTEM.md 存在与否被当作"是否已配置"的标志,因此删除文件才是停用手段——内容为空的文件并不等于未配置。后端的 validate_instruction_content 明确拒绝空内容("Pi instruction cannot be blank; remove the file to deactivate it",见 pi_prompt_files.rs),但 validate_content_size 却放行空模板("empty templates are native"),二者形成了互补的契约:系统提示文件要求非空,而斜杠命令模板允许为空文件(一个空的 /review 模板同样能作为原生入口存在)。
模板命名的可移植性约束
prompts/*.md 的模板名(slug)必须是一个可移植的单个斜杠命令 token(pi_prompt_files.rs),校验规则包括:非空、不超过 128 字节、不能以 . 开头或结尾、禁止空白与控制字符、禁止 <>:"/\|?* 等路径保留字符、并排除 Windows 保留名(CON、PRN、AUX、NUL、COM1-9、LPT1-9)。对应测试覆盖了合法名(review-pr、release.v2、中文 评审)与非法名(with space、a/b、CON、lpt1.txt 等)。
服务层还提供改名能力:upsert(slug, Some(original_slug), ...) 支持"先改名再写入"的原子组合,重命名后的 fs::rename 失败会回滚(见 pi_prompt_files.rs)。
Sessions:可枚举的边界与宽容的解析
会话目录的三种结果
全局会话页只枚举以下三种来源:
- 绝对路径的
sessionDir; ~路径(含~/前缀)——会展开为主目录;- Pi 默认目录:agent 目录下的
sessions/(采用"项目目录"布局,即<root>/<project>/<file>.jsonl)。
而相对 sessionDir(如 .pi/sessions)依赖启动 Pi 时的项目工作目录,CC Switch 没有可靠的启动上下文,因此明确显示"需要项目上下文"(RequiresProjectContext),绝不会猜测目录或回退到其他根目录。会话根解析的完整实现与 session_discovery() 状态机见 session_manager/providers/pi.rs,测试 relative_root_is_explicitly_non_enumerable 断言 .pi/sessions 只能被归类为"需要项目上下文"。
此外,PI_CODING_AGENT_SESSION_DIR 环境变量优先级最高,其次才是全局 settings.json 的 sessionDir,最后回落到默认 agent/sessions。显式配置的目录若不是目录或不可读,会被标记为 Unavailable 并给出具体原因,而不会伪装成"空历史"(对应测试 unavailable_explicit_roots_are_not_reported_as_empty_history)。
JSONL 解析:只消费 UI 所需字段
会话解析只消费 UI 需要的字段,逐行、宽容地读取 Pi 的 JSONL 会话文件,关键规则如下:
- 会话头:首行必须是
type:"session",携带id(需通过树 ID 合法性校验)、cwd、version(0 视为不支持版本,>=1 均可按当前语义读取)与timestamp。 - 树的分支结构:v2+ 条目通过
id/parentId组织;从最新叶子沿parentId链回溯得到活动分支(active entry indexes),被废弃的分支不会出现在消息列表里(测试latest_leaf_defines_the_active_branch验证了这一点);缺失父引用与成环的引用会在可读分支处停止遍历,而不是报错拒绝整个会话。 - 重复 ID:同一会话内重复的条目 ID 以最新条目为准(测试
duplicate_entry_ids_use_the_latest_entry)。 - 会话标题:优先取全局
session_info的name(即使它所在分支已废弃,测试global_name_and_malformed_line_follow_pi_semantics验证该 Pi 语义);没有名称时依次回退到首条用户消息(截断至标题长度上限)或会话目录名。 - 消息类别映射:
message:按role映射user/assistant,toolResult与bashExecution归入tool(后者会拼上$ 命令前缀与输出),branchSummary/compactionSummary归入system;compaction与branch_summary:以摘要内容生成系统消息;custom_message:在display != false时生成系统消息;- 未知条目一律忽略,格式损坏的行直接跳过,不会中断读取。
- 时间戳:消息级
message.timestamp优先于条目级timestamp(与 Pi 会话选择器行为一致),时间格式兼容字符串与毫秒值两种表示。 - 安全上限:单个会话文件最大 128 MiB(
MAX_SESSION_BYTES),单文件最多解析 500,000 个树条目(MAX_TREE_ENTRIES),树 ID 最长 256 字节且只允许字母数字与-、_、.。
删除前的双保险校验
删除会话前必须同时通过两道校验(pi.rs):
- 目标文件仍在当前已解析的 Pi 会话根目录内(
canonicalize后做starts_with前缀检查,并核对活动目录布局:Flat 布局要求相对路径深度为 1,ProjectDirectories 布局要求深度为 2); - 文件内的会话头
id与传入的会话 ID 逐一核对,不一致直接拒绝。
删除前还会重新解析一次根目录并做 canonical 比较,防止"根目录在删除瞬间已被切换"的竞态("Pi session root changed before deletion")。会话恢复命令则统一生成为 pi --session <文件绝对路径>(见 pi.rs,路径经 shell 转义),与 Pi 按精确文件路径恢复会话的行为一致。
明确不做:边界清单与升级策略
契约的另一半是边界。CC Switch 对 Pi 明确不做以下事情:
- Pi
/login、auth.json中的 OAuth / API Key 登录、令牌保存与刷新——认证只属于 Pi 自己; - 默认供应商或默认模型的写入——
defaultProvider/defaultModel永远只读; - 路由、网关、代理、故障转移和请求头合成——这些能力即便在 CC Switch 其他应用中存在,也绝不施加于 Pi 的请求路径,请求始终由 Pi 自己发出;
- Pi 运行时内置供应商与内置模型目录的复制——不把继承/合并产物回写显式配置;
- 完整的
compat、modelOverrides和费用编辑器——思考档位只提供 Pi 原生thinkingLevelMap的轻量入口,对应实现见 src/config/piThinkingProfiles.ts(维护off/minimal/low/medium/high/xhigh/max七档向各厂商原生取值映射的小型 reviewed 档案,缺失键与null有明确差异——空档案被禁止,因为{}保留给用户"显式使用 Pi 默认"的选择)与 src/config/piProviderPresets.ts(独立维护的 Pi 供应商目录,含openai-completions、openai-responses、anthropic-messages、google-generative-ai、bedrock-converse-stream等 API 格式); - 相对会话目录的全局猜测——没有项目上下文就如实报告,不臆测。
当 Pi 发布新版本时,通过正常开发和验收流程重新运行供应商、提示词、Skills 和 Sessions 契约测试即可确认兼容性。没有进入产品界面的上游字段,不应为了"覆盖完整"而扩展后端——这正是本契约能长期保持"小、稳、可验证"的原因。
相关文档与深入阅读
- 前端 Pi 模型目录:src/config/piModelCatalog.ts
- 前端 Pi 思考档位映射:src/config/piThinkingProfiles.ts
- 前端 Pi 供应商预设:src/config/piProviderPresets.ts
- 前端 Pi 供应商表单:src/components/providers/forms/PiProviderForm.tsx
- 原生文件薄适配层:src-tauri/src/pi_config/mod.rs
- 指令文件与斜杠命令服务:src-tauri/src/services/pi_prompt_files.rs
- 只读状态聚合:src-tauri/src/services/pi_state.rs
- 会话发现与 JSONL 解析:src-tauri/src/session_manager/providers/pi.rs
- 思考档位映射需求:docs/pi-thinking-level-map-requirements-zh.md
- 供应商实时同步需求:docs/pi-live-provider-sync-requirements-zh.md
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00