Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/kimi-sync-compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Kimi configuration and database synchronization

The Kimi adapter uses the existing `providers` table with `app_type = "kimi"` and JSON `settings_config`. It does not add tables or columns, or change schema version 18. The native provider attributes are retained in `providerConfig`; each model retains its local alias in `id` and its native attributes (including the upstream `model` and context size) in `config`. Existing flat provider input remains accepted. Cross-provider alias collisions are rejected before writing live configuration.

The database SQL export/import path carries these provider rows unchanged. The regression test covers export for sync, import into another database and reconstruction in an empty Kimi directory. This demonstrates this CLI's round-trip behavior; it does not claim that another upstream client understands Kimi or that all upstream versions retain unknown app rows.

Kimi MCP/Skills enablement is **local deployment state**. The existing shared database has no Kimi enable columns. As with Pi skills, Kimi skill enablement is reconstructed from the native skill directory. MCP enablement is reconstructed from membership in the local `mcp.json`. Restarting retains these states without a schema fork. SQL/WebDAV synchronization carries the shared MCP definitions and skill catalog, but does **not** copy the Kimi enable flags to another machine. Enable the desired definitions on that machine explicitly. Native OAuth files remain managed by Kimi CLI.

## 中文说明

Kimi Provider 继续使用现有 providers 表和 settings_config JSON,数据库 schema 保持 18,不新增表或列。模型别名与真实模型 ID 分开保存,原始扩展字段随配置保留;跨供应商同名模型冲突会在写入前被拒绝。回归测试覆盖数据库同步导出、导入另一数据库以及空目录恢复,但不代表其他上游客户端已支持 Kimi 或保证保留未知 app 数据。

Kimi 的 MCP/Skills 开关属于本机部署状态,从本机 mcp.json 和 skills 目录恢复。重启不会丢失,但数据库/WebDAV 不会将这些开关自动复制到另一机器;目标机器需显式启用。共享定义与 Provider 配置仍走既有数据库同步。Kimi CLI 继续负责原生 OAuth 文件。

## Original CC-Switch revision checked

The upstream named here is **farion1231/cc-switch**, not the CLI fork. At original main `36d950411e622129f285635f43c17e5f35462413`, `src-tauri/src/database/mod.rs:53` declares schema **19**, while this CLI's base declares **18**. The upstream v18→v19 migration adds `enabled_mcode` to MCP/Skills. Its SQL exporter serializes table rows without filtering the provider `app_type`; its AppType enum does not include Kimi. Provider payload storage is opaque JSON, and the providers table has no app-type CHECK constraint.

This PR does not add a further schema divergence. It does **not** resolve the existing fork-wide v19 compatibility gap: a database migrated by the current original application is rejected as too new by this CLI on return. Aligning the shared schema/migrations requires a separate upstream-coordinated change, not a Kimi-only schema increment. Do not advertise latest-original ↔ CLI bidirectional synchronization as verified by this PR.

核对的原版为 farion1231/cc-switch,固定到上述完整 SHA:原版 schema 19、CLI 基线 schema 18。原版升版后的整库回传会触发 CLI 的新版本拒绝保护。这是两项目既有的版本差异,本 Kimi 修复不私自改共享 schema,也不能据此宣称与原版最新版双向同步已经通过。原版尚无 Kimi AppType;其通用 SQL 导出没有按 app_type 过滤 Provider 行。
49 changes: 44 additions & 5 deletions src-tauri/src/app_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ pub struct McpApps {
pub opencode: bool,
#[serde(default)]
pub hermes: bool,
#[serde(default)]
pub kimi: bool,
}

impl McpApps {
Expand All @@ -30,6 +32,7 @@ impl McpApps {
AppType::Hermes => self.hermes,
AppType::OpenClaw => false,
AppType::Pi => false,
AppType::Kimi => self.kimi,
}
}

Expand All @@ -43,6 +46,7 @@ impl McpApps {
AppType::Hermes => self.hermes = enabled,
AppType::OpenClaw => {}
AppType::Pi => {}
AppType::Kimi => self.kimi = enabled,
}
}

Expand All @@ -64,12 +68,15 @@ impl McpApps {
if self.hermes {
apps.push(AppType::Hermes);
}
if self.kimi {
apps.push(AppType::Kimi);
}
apps
}

/// 检查是否所有应用都未启用
pub fn is_empty(&self) -> bool {
!self.claude && !self.codex && !self.gemini && !self.opencode && !self.hermes
!self.claude && !self.codex && !self.gemini && !self.opencode && !self.hermes && !self.kimi
}
}

Expand All @@ -88,6 +95,8 @@ pub struct SkillApps {
pub hermes: bool,
#[serde(default)]
pub pi: bool,
#[serde(default)]
pub kimi: bool,
}

impl SkillApps {
Expand All @@ -100,6 +109,7 @@ impl SkillApps {
AppType::Hermes => self.hermes,
AppType::OpenClaw => false,
AppType::Pi => self.pi,
AppType::Kimi => self.kimi,
}
}

Expand All @@ -112,11 +122,12 @@ impl SkillApps {
AppType::Hermes => self.hermes = enabled,
AppType::OpenClaw => {}
AppType::Pi => self.pi = enabled,
AppType::Kimi => self.kimi = enabled,
}
}

pub fn is_empty(&self) -> bool {
!self.claude && !self.codex && !self.gemini && !self.opencode && !self.hermes && !self.pi
!self.claude && !self.codex && !self.gemini && !self.opencode && !self.hermes && !self.pi && !self.kimi
}

pub fn only(app: &AppType) -> Self {
Expand All @@ -142,6 +153,7 @@ impl SkillApps {
self.opencode |= other.opencode;
self.hermes |= other.hermes;
self.pi |= other.pi;
self.kimi |= other.kimi;
}
}

Expand Down Expand Up @@ -252,6 +264,8 @@ pub struct McpRoot {
pub hermes: McpConfig,
#[serde(default, skip_serializing_if = "McpConfig::is_empty")]
pub openclaw: McpConfig,
#[serde(default, skip_serializing_if = "McpConfig::is_empty")]
pub kimi: McpConfig,
#[serde(skip)]
pub pi: McpConfig,
}
Expand All @@ -268,6 +282,7 @@ impl Default for McpRoot {
opencode: McpConfig::default(),
hermes: McpConfig::default(),
openclaw: McpConfig::default(),
kimi: McpConfig::default(),
pi: McpConfig::default(),
}
}
Expand Down Expand Up @@ -297,6 +312,8 @@ pub struct PromptRoot {
pub openclaw: PromptConfig,
#[serde(default)]
pub pi: PromptConfig,
#[serde(default)]
pub kimi: PromptConfig,
}

use crate::config::{copy_file, get_app_config_dir, get_app_config_path, write_json_file};
Expand All @@ -315,6 +332,7 @@ pub enum AppType {
Hermes,
OpenClaw,
Pi,
Kimi,
}

impl AppType {
Expand All @@ -327,13 +345,14 @@ impl AppType {
AppType::Hermes => "hermes",
AppType::OpenClaw => "openclaw",
AppType::Pi => "pi",
AppType::Kimi => "kimi",
}
}

pub fn is_additive_mode(&self) -> bool {
matches!(
self,
AppType::OpenCode | AppType::Hermes | AppType::OpenClaw | AppType::Pi
AppType::OpenCode | AppType::Hermes | AppType::OpenClaw | AppType::Pi | AppType::Kimi
)
}

Expand All @@ -350,6 +369,7 @@ impl AppType {
AppType::Hermes,
AppType::OpenClaw,
AppType::Pi,
AppType::Kimi,
]
.into_iter()
}
Expand All @@ -374,13 +394,14 @@ impl FromStr for AppType {
"hermes" => Ok(AppType::Hermes),
"openclaw" => Ok(AppType::OpenClaw),
"pi" => Ok(AppType::Pi),
"kimi" => Ok(AppType::Kimi),
other => Err(AppError::localized(
"unsupported_app",
format!(
"不支持的应用标识: '{other}'。可选值: claude, codex, gemini, opencode, hermes, openclaw, pi。"
"不支持的应用标识: '{other}'。可选值: claude, codex, gemini, opencode, hermes, openclaw, pi, kimi。"
),
format!(
"Unsupported app id: '{other}'. Allowed: claude, codex, gemini, opencode, hermes, openclaw, pi."
"Unsupported app id: '{other}'. Allowed: claude, codex, gemini, opencode, hermes, openclaw, pi, kimi."
),
)),
}
Expand All @@ -407,6 +428,9 @@ pub struct CommonConfigSnippets {

#[serde(default, skip_serializing_if = "Option::is_none")]
pub openclaw: Option<String>,

#[serde(default, skip_serializing_if = "Option::is_none")]
pub kimi: Option<String>,
}

impl CommonConfigSnippets {
Expand All @@ -420,6 +444,7 @@ impl CommonConfigSnippets {
AppType::Hermes => self.hermes.as_ref(),
AppType::OpenClaw => self.openclaw.as_ref(),
AppType::Pi => None,
AppType::Kimi => self.kimi.as_ref(),
}
}

Expand All @@ -433,6 +458,7 @@ impl CommonConfigSnippets {
AppType::Hermes => self.hermes = snippet,
AppType::OpenClaw => self.openclaw = snippet,
AppType::Pi => {}
AppType::Kimi => self.kimi = snippet,
}
}
}
Expand Down Expand Up @@ -476,6 +502,7 @@ impl Default for MultiAppConfig {
apps.insert("hermes".to_string(), ProviderManager::default());
apps.insert("openclaw".to_string(), ProviderManager::default());
apps.insert("pi".to_string(), ProviderManager::default());
apps.insert("kimi".to_string(), ProviderManager::default());

Self {
version: 2,
Expand Down Expand Up @@ -590,6 +617,13 @@ impl MultiAppConfig {
updated = true;
}

if !config.apps.contains_key("kimi") {
config
.apps
.insert("kimi".to_string(), ProviderManager::default());
updated = true;
}

// 执行 MCP 迁移(v3.6.x → v3.7.0)
let migrated = config.migrate_mcp_to_unified()?;
if migrated {
Expand Down Expand Up @@ -657,6 +691,7 @@ impl MultiAppConfig {
AppType::Hermes => &self.mcp.hermes,
AppType::OpenClaw => &self.mcp.openclaw,
AppType::Pi => &self.mcp.pi,
AppType::Kimi => &self.mcp.kimi,
}
}

Expand All @@ -670,6 +705,7 @@ impl MultiAppConfig {
AppType::Hermes => &mut self.mcp.hermes,
AppType::OpenClaw => &mut self.mcp.openclaw,
AppType::Pi => &mut self.mcp.pi,
AppType::Kimi => &mut self.mcp.kimi,
}
}

Expand Down Expand Up @@ -699,13 +735,15 @@ impl MultiAppConfig {
AppType::Gemini,
AppType::OpenCode,
AppType::Hermes,
AppType::Kimi,
] {
let old_servers = match app {
AppType::Claude => &self.mcp.claude.servers,
AppType::Codex => &self.mcp.codex.servers,
AppType::Gemini => &self.mcp.gemini.servers,
AppType::OpenCode => &self.mcp.opencode.servers,
AppType::Hermes => &self.mcp.hermes.servers,
AppType::Kimi => &self.mcp.kimi.servers,
AppType::OpenClaw => continue,
AppType::Pi => continue,
};
Expand Down Expand Up @@ -812,6 +850,7 @@ impl MultiAppConfig {
self.mcp.gemini = McpConfig::default();
self.mcp.opencode = McpConfig::default();
self.mcp.hermes = McpConfig::default();
self.mcp.kimi = McpConfig::default();

Ok(true)
}
Expand Down
3 changes: 2 additions & 1 deletion src-tauri/src/cli/commands/config_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ fn canonical_common_snippet(app_type: AppType, raw: &str) -> Result<Option<Strin
| AppType::OpenCode
| AppType::Hermes
| AppType::OpenClaw
| AppType::Pi => {
| AppType::Pi
| AppType::Kimi => {
let value: serde_json::Value = serde_json::from_str(raw).map_err(|e| {
AppError::InvalidInput(texts::tui_toast_invalid_json(&e.to_string()))
})?;
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/cli/commands/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ fn run_app_doctor(app_type: &AppType) -> Result<(), AppError> {
AppType::Claude => check_claude_doctor(),
AppType::Codex => check_codex_doctor(),
AppType::Gemini => check_gemini_doctor(),
AppType::OpenCode | AppType::Hermes | AppType::OpenClaw | AppType::Pi => {
AppType::OpenCode | AppType::Hermes | AppType::OpenClaw | AppType::Pi | AppType::Kimi => {
println!(
"{}",
info(&format!(
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/cli/commands/failover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ fn takeover_enabled_for(takeovers: &ProxyTakeoverStatus, app_type: &AppType) ->
AppType::Claude => takeovers.claude,
AppType::Codex => takeovers.codex,
AppType::Gemini => takeovers.gemini,
AppType::OpenCode | AppType::Hermes | AppType::OpenClaw | AppType::Pi => false,
AppType::OpenCode | AppType::Hermes | AppType::OpenClaw | AppType::Pi | AppType::Kimi => false,
}
}

Expand Down
10 changes: 7 additions & 3 deletions src-tauri/src/cli/commands/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ fn prompt_and_apply_provider_api_format(
match app_type {
AppType::Claude => prompt_and_apply_claude_api_format(app_type, provider),
AppType::Codex => prompt_and_apply_codex_api_format(app_type, provider),
AppType::Gemini | AppType::OpenCode | AppType::Hermes | AppType::OpenClaw | AppType::Pi => {
AppType::Gemini | AppType::OpenCode | AppType::Hermes | AppType::OpenClaw | AppType::Pi | AppType::Kimi => {
Ok(())
}
}
Expand Down Expand Up @@ -1441,7 +1441,7 @@ fn build_add_settings_config(
}
Ok(settings)
}
AppType::OpenCode | AppType::Hermes | AppType::OpenClaw => {
AppType::OpenCode | AppType::Hermes | AppType::OpenClaw | AppType::Kimi => {
let current = current.ok_or_else(|| add_additive_requires_config_error(app_type))?;
let api_key = non_empty(args.api_key.clone());
let base_url = non_empty(args.base_url.clone());
Expand Down Expand Up @@ -1528,7 +1528,7 @@ fn apply_add_provider_api_format(
};
apply_codex_api_format(provider, format);
}
AppType::Gemini | AppType::OpenCode | AppType::Hermes | AppType::OpenClaw | AppType::Pi => {
AppType::Gemini | AppType::OpenCode | AppType::Hermes | AppType::OpenClaw | AppType::Pi | AppType::Kimi => {
}
}
Ok(())
Expand Down Expand Up @@ -1854,6 +1854,10 @@ fn existing_provider_ids_for_duplicate(
.into_iter()
.map(|(id, _)| id)
.collect::<Vec<_>>(),
AppType::Kimi => crate::kimi_config::get_providers()?
.into_iter()
.map(|(id, _)| id)
.collect::<Vec<_>>(),
_ => Vec::new(),
};
ids.extend(live_ids);
Expand Down
Loading
Loading