From 44e0fc9630ac4418e0f5e09f472178c1a74e97f1 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 23 Oct 2026 10:49:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(kimi):=20name=20separate=20official=20acco?= =?UTF-8?q?unts=20"Kimi=20=E8=AE=A2=E9=98=85=20"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kimi Code shows a provider's key as its name in the model picker, so separate accounts appeared as "kimi-code-ccs-". New separate accounts are now keyed "Kimi 订阅 " ("Kimi Subscription" when CC Switch is not in Chinese), matching Kimi's own label for its default login and limited to the characters Kimi accepts, numbered on clashes. The credential slot and model aliases keep the stable "kimi-code-ccs-" name, so default_model stays an ASCII key and ownership checks are unchanged. A returning account is also recognised by its credential slot, not only by the local identity cache. Card names fall back to the readable key when no nickname is cached. --- docs/kimi-code-integration-zh.md | 4 +- src-tauri/src/kimi_config.rs | 33 ++++++ src-tauri/src/services/kimi_oauth/mod.rs | 145 +++++++++++++++++++---- 3 files changed, 157 insertions(+), 25 deletions(-) diff --git a/docs/kimi-code-integration-zh.md b/docs/kimi-code-integration-zh.md index 58c2846f8..1bc395488 100644 --- a/docs/kimi-code-integration-zh.md +++ b/docs/kimi-code-integration-zh.md @@ -34,7 +34,7 @@ Ollama、LM Studio 和 vLLM 预设使用各自的本地 OpenAI 兼容地址, ## 官方 OAuth 登录 -官方账号供应商使用 Kimi 图标并带有「官方」徽标。默认账号沿用原生 `managed:kimi-code` 供应商,显示为 **Kimi Official**;其余账号使用独立的 `kimi-code-ccs-` 供应商 ID,显示为 **Kimi Official (账号标识)**。已有原生登录保持原 ID、模型别名和凭据引用,自动显示在授权中心;导入时补齐名称、图标和官方分类,自定义显示信息保留。 +官方账号供应商使用 Kimi 图标并带有「官方」徽标。默认账号沿用原生 `managed:kimi-code` 供应商,显示为 **Kimi Official**;其余账号的供应商 ID 取 `Kimi 订阅 <昵称>`(英文界面为 `Kimi Subscription <昵称>`,与 Kimi Code 给自己默认登录写死的「Kimi 订阅」并列;Kimi Code 把供应商 ID 当名称显示在模型选择器的分组上;只保留字母、数字、`-`、`_` 和空格,重名时补序号),卡片显示为 **Kimi Official · 昵称**;读不到昵称时退回 `kimi-code-ccs-<标识>`。供应商 ID 在创建后不随昵称改动;两台设备上昵称不同时会各得一个 ID。已有原生登录保持原 ID、模型别名和凭据引用,自动显示在授权中心;导入时补齐名称、图标和官方分类,自定义显示信息保留。 打开「授权中心」的 **Kimi Code** 分组,选择国内版或海外版后点击「登录 Kimi」或「添加账号」,在浏览器中输入授权码并确认。若浏览器没有自动打开,可使用页面上的授权链接和复制授权码按钮。「添加供应商 → 账号登录 → Kimi Code 官方登录」提供「管理 Kimi 账号」入口,进入同一个授权中心。此流程无需填写 API Key、模型或 JSON 配置,也不显示普通供应商表单的「添加」按钮。 @@ -47,7 +47,7 @@ Ollama、LM Studio 和 vLLM 预设使用各自的本地 OpenAI 兼容地址, 授权成功后,CC Switch 从官方 `/models` 获取该账号的模型和能力。原生默认供应商不存在或其凭据文件缺失时,新登录沿用 `managed:kimi-code` 和官方默认 OAuth 引用,模型别名为 `kimi-code/<模型 ID>`;默认登录已存在时,新增账号使用独立供应商和 `<供应商 ID>/<模型 ID>` 模型别名。缺失的联网搜索和抓取服务也会添加。已有默认模型、用户别名、模型 overrides、自定义服务及其他账号配置会保留;没有默认模型时首次登录会选中该账号的模型。配置或凭据保存失败时恢复原配置,可点击「重试」继续完成当前授权。 -凭据按官方格式保存在 `~/.kimi-code/credentials/`,仅后端读写,不进入供应商档案或前端响应。原生国内默认登录使用 `kimi-code.json`,海外版按官方规则选择独立的默认槽位;其余账号各自使用 `kimi-code-ccs-<8 位标识>.json`(标识由区域和账号 ID 算出,同一账号在不同设备上相同;早期版本创建的 UUID 槽位照常识别),通过供应商的 `oauth.key` 与 `oauth.oauth_host` 引用。`KIMI_CODE_HOME` 仍用于选择数据目录;CC Switch 登录不使用 `KIMI_CODE_OAUTH_HOST`、`KIMI_OAUTH_HOST` 或 `KIMI_CODE_BASE_URL` 覆盖所选区域的地址。重新登录已有账号时保持该账号的区域与凭据槽位,使用该区域的固定官方地址。CC Switch 只在查询额度、令牌快过期时刷新,刷新前拿 Kimi 同款的凭据锁(`oauth/<槽位>.lock` 目录,5 秒未更新视为失效),锁内重读凭据,与 Kimi Code 不会同时轮换 refresh token;刷新被拒时按 Kimi 的规则先重读,别的进程已换新就沿用,否则写入作废标记、该账号需重新登录。Windows 上 Kimi Code 不加锁,CC Switch 也不刷新,令牌过期时额度提示先在 Kimi Code 里用一次该账号。登录、删除写凭据时同样持锁。 +凭据按官方格式保存在 `~/.kimi-code/credentials/`,仅后端读写,不进入供应商档案或前端响应。原生国内默认登录使用 `kimi-code.json`,海外版按官方规则选择独立的默认槽位;其余账号各自使用 `kimi-code-ccs-<8 位标识>.json`(标识由区域和账号 ID 算出,同一账号在不同设备上相同;早期版本创建的 UUID 槽位照常识别),模型别名也以这个槽位名为前缀(如 `kimi-code-ccs-<标识>/k3`),`default_model` 始终是 ASCII 键;认同一个账号时既看本机身份缓存,也看凭据槽位名,通过供应商的 `oauth.key` 与 `oauth.oauth_host` 引用。`KIMI_CODE_HOME` 仍用于选择数据目录;CC Switch 登录不使用 `KIMI_CODE_OAUTH_HOST`、`KIMI_OAUTH_HOST` 或 `KIMI_CODE_BASE_URL` 覆盖所选区域的地址。重新登录已有账号时保持该账号的区域与凭据槽位,使用该区域的固定官方地址。CC Switch 只在查询额度、令牌快过期时刷新,刷新前拿 Kimi 同款的凭据锁(`oauth/<槽位>.lock` 目录,5 秒未更新视为失效),锁内重读凭据,与 Kimi Code 不会同时轮换 refresh token;刷新被拒时按 Kimi 的规则先重读,别的进程已换新就沿用,否则写入作废标记、该账号需重新登录。Windows 上 Kimi Code 不加锁,CC Switch 也不刷新,令牌过期时额度提示先在 Kimi Code 里用一次该账号。登录、删除写凭据时同样持锁。 网络请求沿用 CC Switch 的全局出站代理设置。连接失败时显示授权主机及底层 DNS、TCP、TLS 或超时原因,便于核对系统代理和网络连接。 diff --git a/src-tauri/src/kimi_config.rs b/src-tauri/src/kimi_config.rs index 38c478f52..b71c73a20 100644 --- a/src-tauri/src/kimi_config.rs +++ b/src-tauri/src/kimi_config.rs @@ -28,6 +28,12 @@ pub(crate) fn official_provider_name(id: &str, nickname: Option<&str>) -> String format!("{OFFICIAL_PROVIDER_NAME} · {nickname}") } else if id == OFFICIAL_PROVIDER_ID { OFFICIAL_PROVIDER_NAME.into() + } else if let Some(name) = ["Kimi 订阅 ", "Kimi Subscription ", "Kimi "] + .iter() + .find_map(|prefix| id.strip_prefix(prefix)) + { + // Separate accounts are keyed `Kimi 订阅 ` (see kimi_oauth::new_account). + format!("{OFFICIAL_PROVIDER_NAME} · {name}") } else { let account = id.strip_prefix("kimi-code-ccs-").unwrap_or(id); format!( @@ -720,6 +726,33 @@ pub(crate) fn save_oauth_login( mod tests { use super::*; + #[test] + fn official_card_names_follow_nickname_then_provider_key() { + assert_eq!( + official_provider_name("Kimi 订阅 登月者9287", Some("新昵称")), + "Kimi Official · 新昵称" + ); + // Without a cached nickname the readable key still names the card. + for id in [ + "Kimi 订阅 登月者9287", + "Kimi Subscription 登月者9287", + "Kimi 登月者9287", + ] { + assert_eq!( + official_provider_name(id, None), + "Kimi Official · 登月者9287" + ); + } + assert_eq!( + official_provider_name("kimi-code-ccs-fd019f9e", None), + "Kimi Official (fd019f9e)" + ); + assert_eq!( + official_provider_name(OFFICIAL_PROVIDER_ID, None), + "Kimi Official" + ); + } + #[test] fn official_identity_repairs_imported_name_icon_and_category_without_renaming_native_keys() { let config = json!({"provider":{"type":"kimi","base_url":"https://api.kimi.com/coding/v1","oauth":{"storage":"file","key":"oauth/kimi-code"}},"models":{}}); diff --git a/src-tauri/src/services/kimi_oauth/mod.rs b/src-tauri/src/services/kimi_oauth/mod.rs index bad230892..77a194983 100644 --- a/src-tauri/src/services/kimi_oauth/mod.rs +++ b/src-tauri/src/services/kimi_oauth/mod.rs @@ -7,9 +7,11 @@ mod lock; use std::{ fs, path::{Path, PathBuf}, + sync::LazyLock, time::{Duration, Instant}, }; +use regex::Regex; use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; use sha2::{Digest, Sha256}; @@ -231,7 +233,17 @@ fn read_config(home: &Path) -> Result { kimi_config::read(&home.join("config.toml")).map_err(|error| error.to_string()) } -fn new_account(home: &Path, endpoints: &Endpoints, id: &str) -> Result<(String, Value), String> { +/// A new account goes into Kimi's default slot while that is free; otherwise it +/// gets its own credential slot `kimi-code-ccs-`. Kimi shows a +/// provider's key as its name, so a separate account's key is +/// `Kimi 订阅 ` when the nickname is known; the slot name stays the +/// stable identity. +fn new_account( + home: &Path, + endpoints: &Endpoints, + suffix: &str, + nickname: Option<&str>, +) -> Result<(String, Value), String> { let config = read_config(home)?; let provider = &config["providers"][PROVIDER_ID]; let native_oauth = endpoints.oauth_ref(); @@ -244,14 +256,50 @@ fn new_account(home: &Path, endpoints: &Endpoints, id: &str) -> Result<(String, .try_exists() .map_err(|e| format!("Cannot check Kimi Code credentials: {e}"))?); if occupied { - let id = format!("kimi-code-ccs-{id}"); - let oauth = json!({"storage": "file", "key": format!("oauth/{id}"), "oauth_host": endpoints.oauth_host}); - Ok((id, oauth)) + let slot = format!("kimi-code-ccs-{suffix}"); + let oauth = json!({"storage": "file", "key": format!("oauth/{slot}"), "oauth_host": endpoints.oauth_host}); + let id = readable_provider_id(&config, &subscription_label(), nickname); + Ok((id.unwrap_or(slot), oauth)) } else { Ok((PROVIDER_ID.into(), native_oauth)) } } +/// Kimi's own label for its default login ("Kimi 订阅" / "Kimi Subscription"), +/// in CC Switch's language, so separate accounts sit next to it in the model +/// picker as `Kimi 订阅 `. Kimi only ships Chinese and English. +fn subscription_label() -> String { + if crate::tray::tray_language().starts_with("zh") { + "Kimi 订阅".into() + } else { + "Kimi Subscription".into() + } +} + +/// `