mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat(grokbuild): add Grok Official provider with official-state import
Add a "Grok Official" preset and seed (grokbuild-official) whose empty config represents the official login state: no custom [model.*] tables are written, so Grok CLI falls back to its built-in xAI OAuth login and cc-switch never touches those credentials. Backend: - Seed entry in OFFICIAL_SEEDS plus ensure_grokbuild_official_provider command for on-demand repair (the one-shot master seeding flag is already set for existing databases). - Split validation into syntax-only (empty allowed) for live reads, writes and official snapshots, keeping the full custom-model shape check for non-official provider writes and imports. Backup/restore can now round-trip an official-state live file. - Manual import (command layer only) recognizes an official-state live config and imports it as the official entry set as current, matching the Codex official-login import outcome. Startup auto-import keeps rejecting official-state live so a deleted official entry is never resurrected on launch: startup import only captures real user data as "default" and never manufactures official entries. - Manual import also ensures the official entry before importing (claude-desktop precedent) and after a successful custom import, so first-time users end up with default + official like other apps. - Proxy takeover guards skip or reject official-state live configs in all three takeover paths, consistent with the official-provider takeover ban. Frontend: - Grok Official preset entry in the GrokBuild form: official category hides connection fields and passes the raw config through untouched. - Filter managed-OAuth presets out of the GrokBuild preset list; they were never wired for this app and produced keyless broken configs. Tests cover seed presence, official round-trip, ensure-after-deletion, and the four import scenarios including startup non-resurrection.
This commit is contained in:
@@ -117,6 +117,46 @@ pub async fn switch_provider(
|
||||
}
|
||||
|
||||
fn import_default_config_internal(state: &AppState, app_type: AppType) -> Result<bool, AppError> {
|
||||
if matches!(app_type, AppType::GrokBuild) {
|
||||
// 官方登录态(live 语法合法且无自定义模型表)+ 用户手动导入:
|
||||
// 导入的正确结果是让 Grok Official 成为当前供应商,而非报错。
|
||||
// 只挂在命令层 = 只有手动动作可达;启动自动导入走 service 层、
|
||||
// 官方态照旧报错静默跳过,删掉的官方条目不会被重启复活
|
||||
//(全项目惯例:启动自动导入只产出 default,从不产出官方条目)。
|
||||
if let Ok(settings) = crate::grok_config::read_grok_live_settings() {
|
||||
let config = settings
|
||||
.get("config")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
if crate::grok_config::is_official_live_config(config) {
|
||||
state.db.ensure_official_seed_by_id(
|
||||
crate::database::GROKBUILD_OFFICIAL_PROVIDER_ID,
|
||||
AppType::GrokBuild,
|
||||
)?;
|
||||
state.db.set_current_provider(
|
||||
app_type.as_str(),
|
||||
crate::database::GROKBUILD_OFFICIAL_PROVIDER_ID,
|
||||
)?;
|
||||
crate::settings::set_current_provider(
|
||||
&app_type,
|
||||
Some(crate::database::GROKBUILD_OFFICIAL_PROVIDER_ID),
|
||||
)?;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Safety net: 与 claude-desktop 导入同语义 —— 用户主动点导入是"重新
|
||||
// 整理该表"的隐式信号,把官方入口补回来。覆盖导入必然失败的场景
|
||||
//(live 文件缺失 / TOML 语法错误 / 残缺的自定义配置),避免
|
||||
// "报错 + 空列表"死胡同。失败只 warn,不影响导入主流程。
|
||||
if let Err(e) = state.db.ensure_official_seed_by_id(
|
||||
crate::database::GROKBUILD_OFFICIAL_PROVIDER_ID,
|
||||
AppType::GrokBuild,
|
||||
) {
|
||||
log::warn!("Failed to ensure grokbuild-official seed during import: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
let imported = ProviderService::import_default_config(state, app_type.clone())?;
|
||||
|
||||
if imported {
|
||||
@@ -246,6 +286,17 @@ pub fn ensure_codex_official_provider(state: State<'_, AppState>) -> Result<bool
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn ensure_grokbuild_official_provider(state: State<'_, AppState>) -> Result<bool, String> {
|
||||
state
|
||||
.db
|
||||
.ensure_official_seed_by_id(
|
||||
crate::database::GROKBUILD_OFFICIAL_PROVIDER_ID,
|
||||
AppType::GrokBuild,
|
||||
)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn claude_provider_models_are_claude_safe(provider: &Provider) -> bool {
|
||||
let Some(env) = provider
|
||||
.settings_config
|
||||
|
||||
@@ -712,6 +712,7 @@ mod ensure_official_seed_tests {
|
||||
use crate::app_config::AppType;
|
||||
use crate::database::{
|
||||
Database, CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID, CODEX_OFFICIAL_PROVIDER_ID,
|
||||
GROKBUILD_OFFICIAL_PROVIDER_ID,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -790,6 +791,26 @@ mod ensure_official_seed_tests {
|
||||
assert_eq!(provider.settings_config["auth"], serde_json::json!({}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_recreates_grokbuild_official_seed_after_deletion() {
|
||||
let db = Database::memory().expect("memory db");
|
||||
db.init_default_official_providers().expect("seed");
|
||||
db.delete_provider(AppType::GrokBuild.as_str(), GROKBUILD_OFFICIAL_PROVIDER_ID)
|
||||
.expect("delete Grok Build official");
|
||||
|
||||
let inserted = db
|
||||
.ensure_official_seed_by_id(GROKBUILD_OFFICIAL_PROVIDER_ID, AppType::GrokBuild)
|
||||
.expect("ensure Grok Build official");
|
||||
assert!(inserted);
|
||||
let provider = db
|
||||
.get_provider_by_id(GROKBUILD_OFFICIAL_PROVIDER_ID, AppType::GrokBuild.as_str())
|
||||
.expect("query")
|
||||
.expect("Grok Build official restored");
|
||||
assert_eq!(provider.category.as_deref(), Some("official"));
|
||||
// 空 config:切换时不注入自定义模型表,Grok CLI 回落到自带 OAuth 登录
|
||||
assert_eq!(provider.settings_config["config"], serde_json::json!(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_rejects_unknown_seed() {
|
||||
let db = Database::memory().expect("memory db");
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
//! - `src/config/claudeProviderPresets.ts`("Claude Official")
|
||||
//! - `src/config/codexProviderPresets.ts`("OpenAI Official")
|
||||
//! - `src/config/geminiProviderPresets.ts`("Google Official")
|
||||
//! - `src/components/providers/forms/GrokBuildProviderForm.tsx`("Grok Official")
|
||||
|
||||
use crate::app_config::AppType;
|
||||
|
||||
pub(crate) const CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID: &str = "claude-desktop-official";
|
||||
pub(crate) const CODEX_OFFICIAL_PROVIDER_ID: &str = "codex-official";
|
||||
pub(crate) const GROKBUILD_OFFICIAL_PROVIDER_ID: &str = "grokbuild-official";
|
||||
|
||||
/// 单条官方供应商种子定义。
|
||||
pub(crate) struct OfficialProviderSeed {
|
||||
@@ -69,6 +71,16 @@ pub(crate) const OFFICIAL_SEEDS: &[OfficialProviderSeed] = &[
|
||||
// 空 env + 空 config 让用户走 Google OAuth
|
||||
settings_config_json: r#"{"env":{},"config":{}}"#,
|
||||
},
|
||||
OfficialProviderSeed {
|
||||
id: GROKBUILD_OFFICIAL_PROVIDER_ID,
|
||||
app_type: AppType::GrokBuild,
|
||||
name: "Grok Official",
|
||||
website_url: "https://x.ai/grok",
|
||||
icon: "grok",
|
||||
icon_color: "currentColor",
|
||||
// 空 config = 不写自定义模型表,Grok CLI 回落到自带的 xAI OAuth 登录
|
||||
settings_config_json: r#"{"config":""}"#,
|
||||
},
|
||||
];
|
||||
|
||||
/// 判断给定的 provider id 是否属于内置官方种子。
|
||||
@@ -92,4 +104,17 @@ mod tests {
|
||||
assert_eq!(seed.app_type, AppType::ClaudeDesktop);
|
||||
assert!(is_official_seed_id(CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn official_seeds_include_grokbuild() {
|
||||
let seed = OFFICIAL_SEEDS
|
||||
.iter()
|
||||
.find(|seed| seed.id == GROKBUILD_OFFICIAL_PROVIDER_ID)
|
||||
.expect("grok build official seed");
|
||||
|
||||
assert_eq!(seed.app_type, AppType::GrokBuild);
|
||||
assert!(is_official_seed_id(GROKBUILD_OFFICIAL_PROVIDER_ID));
|
||||
// 空 config = 官方登录态:切换时不注入自定义模型表
|
||||
assert_eq!(seed.settings_config_json, r#"{"config":""}"#);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ mod tests;
|
||||
// DAO 类型导出供外部使用
|
||||
pub(crate) use dao::providers_seed::{
|
||||
is_official_seed_id, CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID, CODEX_OFFICIAL_PROVIDER_ID,
|
||||
GROKBUILD_OFFICIAL_PROVIDER_ID,
|
||||
};
|
||||
pub(crate) use dao::proxy::{
|
||||
validate_cost_multiplier, validate_pricing_source, PRICING_SOURCE_REQUEST,
|
||||
|
||||
@@ -59,6 +59,42 @@ fn optional_non_empty_string(table: &toml::value::Table, key: &str) -> Option<St
|
||||
.map(ToString::to_string)
|
||||
}
|
||||
|
||||
/// Syntax-only validation for a Grok Build config document (empty allowed).
|
||||
///
|
||||
/// 官方条目走 Grok CLI 自带的 xAI OAuth 登录,config.toml 不需要(通常也没有)
|
||||
/// 自定义模型表:空文档合法,非空只要求 TOML 语法合法。live 层的读写与官方
|
||||
/// 快照校验都用它;"必须有完整自定义模型表"的强校验见 `validate_config_toml`。
|
||||
pub fn validate_config_toml_syntax(config_toml: &str) -> Result<(), AppError> {
|
||||
if config_toml.trim().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
config_toml
|
||||
.parse::<toml::Value>()
|
||||
.map(|_| ())
|
||||
.map_err(|error| {
|
||||
AppError::localized(
|
||||
"provider.grokbuild.config.invalid_toml",
|
||||
format!("Grok Build config.toml 格式错误: {error}"),
|
||||
format!("Invalid Grok Build config.toml: {error}"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether a live config document represents the official login state.
|
||||
///
|
||||
/// 官方态 = 语法合法且完全没有自定义模型痕迹(无 `[models]` 也无 `[model.*]`,
|
||||
/// 允许 `[mcp_servers]` 等其它内容)。只要出现过任一自定义键就返回 false,
|
||||
/// 让残缺的自定义配置继续走 `validate_config_toml` 报出真实错误,
|
||||
/// 而不是被误判成官方态静默吞掉。语法不合法同样返回 false。
|
||||
pub fn is_official_live_config(config_toml: &str) -> bool {
|
||||
let Ok(document) = config_toml.parse::<toml::Value>() else {
|
||||
return false;
|
||||
};
|
||||
document
|
||||
.as_table()
|
||||
.is_some_and(|root| !root.contains_key("models") && !root.contains_key("model"))
|
||||
}
|
||||
|
||||
/// Validate the provider-owned Grok Build TOML document.
|
||||
pub fn validate_config_toml(config_toml: &str) -> Result<(), AppError> {
|
||||
let document = config_toml.parse::<toml::Value>().map_err(|error| {
|
||||
@@ -305,6 +341,11 @@ pub fn strip_grok_mcp_servers_from_settings(settings: &mut Value) -> Result<(),
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read the live `~/.grok/config.toml` as a provider settings snapshot.
|
||||
///
|
||||
/// 只做 TOML 语法校验:live 处于官方态(无自定义模型表)时同样需要能被
|
||||
/// 读取,供切换回填与界面展示使用。需要"完整自定义模型配置"的导入路径
|
||||
/// 由调用方自行叠加 `validate_config_toml`。
|
||||
pub fn read_grok_live_settings() -> Result<Value, AppError> {
|
||||
let path = get_grok_config_path();
|
||||
if !path.exists() {
|
||||
@@ -316,7 +357,7 @@ pub fn read_grok_live_settings() -> Result<Value, AppError> {
|
||||
}
|
||||
|
||||
let config = fs::read_to_string(&path).map_err(|error| AppError::io(&path, error))?;
|
||||
validate_config_toml(&config)?;
|
||||
validate_config_toml_syntax(&config)?;
|
||||
Ok(json!({ "config": config }))
|
||||
}
|
||||
|
||||
@@ -339,9 +380,20 @@ pub fn write_grok_provider_live(provider: &Provider) -> Result<(), AppError> {
|
||||
)
|
||||
})?;
|
||||
|
||||
// 官方条目不注入自定义模型表:按快照原样写回(首次为空文件),
|
||||
// Grok CLI 回落到官方内置模型 + 自带 OAuth 登录;MCP 投影随后由
|
||||
// 切换流程重新补写。非官方供应商必须携带完整的自定义模型配置。
|
||||
if provider.category.as_deref() != Some("official") {
|
||||
validate_config_toml(config)?;
|
||||
}
|
||||
|
||||
write_grok_live_settings(&json!({ "config": config }))
|
||||
}
|
||||
|
||||
/// Raw live-file writer, mirroring `read_grok_live_settings` (syntax-only).
|
||||
///
|
||||
/// 代理接管的备份/恢复也走这里:官方态 live(无自定义模型表)必须可以
|
||||
/// 原样写回。完整形状校验由 `write_grok_provider_live` 的非官方分支负责。
|
||||
pub fn write_grok_live_settings(settings: &Value) -> Result<(), AppError> {
|
||||
let config = settings
|
||||
.get("config")
|
||||
@@ -353,7 +405,7 @@ pub fn write_grok_live_settings(settings: &Value) -> Result<(), AppError> {
|
||||
"Grok Build configuration is missing the config field",
|
||||
)
|
||||
})?;
|
||||
validate_config_toml(config)?;
|
||||
validate_config_toml_syntax(config)?;
|
||||
write_text_file(&get_grok_config_path(), config)
|
||||
}
|
||||
|
||||
@@ -397,6 +449,32 @@ context_window = 500000
|
||||
validate_config_toml(valid_env_key_config()).expect("valid env_key configuration");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_validation_accepts_official_snapshots() {
|
||||
validate_config_toml_syntax("").expect("empty official snapshot");
|
||||
validate_config_toml_syntax("[mcp_servers.echo]\ncommand = \"echo\"\n")
|
||||
.expect("official-mode config without model tables");
|
||||
assert!(validate_config_toml_syntax("not = [valid").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn official_live_config_detection() {
|
||||
// 官方态:完全没有自定义模型痕迹
|
||||
assert!(is_official_live_config(""));
|
||||
assert!(is_official_live_config(" \n# comment only\n"));
|
||||
assert!(is_official_live_config(
|
||||
"[mcp_servers.echo]\ncommand = \"echo\"\n"
|
||||
));
|
||||
|
||||
// 出现过任一自定义键(哪怕残缺)都不是官方态,交给强校验报错
|
||||
assert!(!is_official_live_config(valid_config()));
|
||||
assert!(!is_official_live_config("[models]\ndefault = \"x\"\n"));
|
||||
assert!(!is_official_live_config("[model.x]\nmodel = \"x\"\n"));
|
||||
|
||||
// 语法不合法不是官方态
|
||||
assert!(!is_official_live_config("not = [valid"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_missing_selected_model_table() {
|
||||
let error = validate_config_toml("[models]\ndefault = \"grok-4.5\"\n")
|
||||
@@ -479,6 +557,52 @@ context_window = 500000
|
||||
validate_config_toml(config).expect("stripped config remains valid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn official_provider_roundtrips_without_custom_model_tables() {
|
||||
let temp = TempDir::new().expect("temp dir");
|
||||
let original_test_home = std::env::var_os("CC_SWITCH_TEST_HOME");
|
||||
std::env::set_var("CC_SWITCH_TEST_HOME", temp.path());
|
||||
|
||||
// 官方条目:空 config 可写(清掉自定义模型表,交还 Grok CLI 官方登录)
|
||||
let mut official = Provider::with_id(
|
||||
"grokbuild-official".to_string(),
|
||||
"Grok Official".to_string(),
|
||||
json!({ "config": "" }),
|
||||
None,
|
||||
);
|
||||
official.category = Some("official".to_string());
|
||||
write_grok_provider_live(&official).expect("official empty config is writable");
|
||||
assert_eq!(
|
||||
fs::read_to_string(get_grok_config_path()).expect("read config"),
|
||||
""
|
||||
);
|
||||
|
||||
// 官方态 live(如 MCP 投影补写后)无自定义模型表,读取与原样写回都必须可用
|
||||
let official_live = "[mcp_servers.echo]\ncommand = \"echo\"\n";
|
||||
write_grok_live_settings(&json!({ "config": official_live }))
|
||||
.expect("official-mode live is writable for backup restore");
|
||||
let settings = read_grok_live_settings().expect("official-mode live is readable");
|
||||
assert_eq!(
|
||||
settings.get("config").and_then(Value::as_str),
|
||||
Some(official_live)
|
||||
);
|
||||
|
||||
// 非官方供应商仍要求完整的自定义模型配置
|
||||
let custom = Provider::with_id(
|
||||
"custom".to_string(),
|
||||
"Custom".to_string(),
|
||||
json!({ "config": "" }),
|
||||
None,
|
||||
);
|
||||
assert!(write_grok_provider_live(&custom).is_err());
|
||||
|
||||
match original_test_home {
|
||||
Some(value) => std::env::set_var("CC_SWITCH_TEST_HOME", value),
|
||||
None => std::env::remove_var("CC_SWITCH_TEST_HOME"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn writes_and_reads_live_config() {
|
||||
|
||||
@@ -47,6 +47,7 @@ pub use config::{get_claude_mcp_path, get_claude_settings_path, read_json_file};
|
||||
pub use database::{Database, Profile};
|
||||
pub use deeplink::{import_provider_from_deeplink, parse_deeplink_url, DeepLinkImportRequest};
|
||||
pub use error::AppError;
|
||||
pub use grok_config::get_grok_config_path;
|
||||
pub use mcp::{
|
||||
import_from_claude, import_from_codex, import_from_gemini, import_from_grokbuild,
|
||||
remove_server_from_claude, remove_server_from_codex, remove_server_from_gemini,
|
||||
@@ -1316,6 +1317,7 @@ pub fn run() {
|
||||
commands::import_claude_desktop_providers_from_claude,
|
||||
commands::ensure_claude_desktop_official_provider,
|
||||
commands::ensure_codex_official_provider,
|
||||
commands::ensure_grokbuild_official_provider,
|
||||
commands::get_claude_config_status,
|
||||
commands::get_config_status,
|
||||
commands::get_claude_code_config_path,
|
||||
|
||||
@@ -1459,6 +1459,16 @@ pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<bool
|
||||
AppType::Codex => crate::codex_config::read_codex_live_settings()?,
|
||||
AppType::GrokBuild => {
|
||||
let mut settings = crate::grok_config::read_grok_live_settings()?;
|
||||
let config = settings
|
||||
.get("config")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
// 官方登录态(无自定义模型表)在这里必须报错:本函数也被启动
|
||||
// 自动导入调用,而全项目惯例是"启动自动导入只产出 default,
|
||||
// 从不产出官方条目"——否则删掉的官方条目每次重启都会复活。
|
||||
// 官方态的成功导入(补官方条目并激活)只挂在手动导入的命令层
|
||||
// (`import_default_config_internal`)。
|
||||
crate::grok_config::validate_config_toml(config)?;
|
||||
crate::grok_config::strip_grok_mcp_servers_from_settings(&mut settings)?;
|
||||
settings
|
||||
}
|
||||
@@ -1560,6 +1570,20 @@ pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<bool
|
||||
.set_current_provider(app_type.as_str(), &provider.id)?;
|
||||
crate::settings::set_current_provider(&app_type, Some(provider.id.as_str()))?;
|
||||
|
||||
// 初次导入已有配置时随手补出官方入口,对齐其它应用"首启动 = 导入 default
|
||||
// + 播种官方条目"的观感。grokbuild 种子晚于 `official_providers_seeded`
|
||||
// flag 引入,存量库的主播种不会再跑,只能挂在导入动作上补。
|
||||
// 只在导入成功时执行;live 完全不可导入(文件缺失/语法错误/残缺配置)
|
||||
// 不会到达这里。失败只 warn。
|
||||
if matches!(app_type, AppType::GrokBuild) {
|
||||
if let Err(e) = state.db.ensure_official_seed_by_id(
|
||||
crate::database::GROKBUILD_OFFICIAL_PROVIDER_ID,
|
||||
AppType::GrokBuild,
|
||||
) {
|
||||
log::warn!("Failed to ensure grokbuild-official seed after import: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true) // 真正导入了
|
||||
}
|
||||
|
||||
|
||||
@@ -3501,7 +3501,13 @@ impl ProviderService {
|
||||
"Grok Build configuration is missing the config field",
|
||||
)
|
||||
})?;
|
||||
crate::grok_config::validate_config_toml(config)?;
|
||||
if provider.category.as_deref() == Some("official") {
|
||||
// 官方条目走 Grok CLI 自带 OAuth:空 config 合法,
|
||||
// 回填快照只要求 TOML 语法合法。
|
||||
crate::grok_config::validate_config_toml_syntax(config)?;
|
||||
} else {
|
||||
crate::grok_config::validate_config_toml(config)?;
|
||||
}
|
||||
}
|
||||
AppType::OpenCode => {
|
||||
// OpenCode uses a different config structure: { npm, options, models }
|
||||
|
||||
@@ -1502,6 +1502,19 @@ impl ProxyService {
|
||||
Ok((proxy_url, proxy_codex_base_url))
|
||||
}
|
||||
|
||||
/// Grok Build live 是否具备可接管的自定义模型表。
|
||||
///
|
||||
/// 官方态 live(Grok CLI 自带 OAuth 登录、无 `[model.*]` 表)没有注入
|
||||
/// 占位符的落点,且官方供应商本就禁止经代理接管(封号风险),调用方
|
||||
/// 应跳过接管或直接报错。
|
||||
fn grok_live_config_supports_takeover(config: &Value) -> bool {
|
||||
config
|
||||
.get("config")
|
||||
.and_then(Value::as_str)
|
||||
.and_then(crate::grok_config::extract_model_config)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn apply_grok_takeover_fields(config: &mut Value, proxy_base_url: &str) -> Result<(), String> {
|
||||
let config_toml = config
|
||||
.get("config")
|
||||
@@ -1573,9 +1586,13 @@ impl ProxyService {
|
||||
|
||||
// Grok Build: keep its own provider namespace while reusing Responses forwarding.
|
||||
if let Ok(mut live_config) = self.read_grok_live() {
|
||||
Self::apply_grok_takeover_fields(&mut live_config, &proxy_grok_base_url)?;
|
||||
self.write_grok_live(&live_config)?;
|
||||
log::info!("Grok Build Live 配置已接管,代理地址: {proxy_grok_base_url}");
|
||||
if Self::grok_live_config_supports_takeover(&live_config) {
|
||||
Self::apply_grok_takeover_fields(&mut live_config, &proxy_grok_base_url)?;
|
||||
self.write_grok_live(&live_config)?;
|
||||
log::info!("Grok Build Live 配置已接管,代理地址: {proxy_grok_base_url}");
|
||||
} else {
|
||||
log::info!("Grok Build Live 处于官方登录态(无自定义模型表),跳过代理接管");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1630,6 +1647,14 @@ impl ProxyService {
|
||||
}
|
||||
AppType::GrokBuild => {
|
||||
let mut live_config = self.read_grok_live()?;
|
||||
if !Self::grok_live_config_supports_takeover(&live_config) {
|
||||
return Err(
|
||||
"Grok Build 当前为官方登录态(无自定义模型表),官方供应商不支持代理接管 \
|
||||
(Grok Build is using the official login without a custom model table; \
|
||||
official providers cannot be taken over by the proxy)"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
Self::apply_grok_takeover_fields(&mut live_config, &proxy_grok_base_url)?;
|
||||
self.write_grok_live(&live_config)?;
|
||||
log::info!("Grok Build Live 配置已接管,代理地址: {proxy_grok_base_url}");
|
||||
@@ -1701,8 +1726,14 @@ impl ProxyService {
|
||||
}
|
||||
AppType::GrokBuild => {
|
||||
if let Ok(mut live_config) = self.read_grok_live() {
|
||||
Self::apply_grok_takeover_fields(&mut live_config, &proxy_grok_base_url)?;
|
||||
let _ = self.write_grok_live(&live_config);
|
||||
if Self::grok_live_config_supports_takeover(&live_config) {
|
||||
Self::apply_grok_takeover_fields(&mut live_config, &proxy_grok_base_url)?;
|
||||
let _ = self.write_grok_live(&live_config);
|
||||
} else {
|
||||
log::info!(
|
||||
"Grok Build Live 处于官方登录态(无自定义模型表),跳过代理接管"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -4,9 +4,9 @@ use std::fs;
|
||||
use serde_json::json;
|
||||
|
||||
use cc_switch_lib::{
|
||||
get_claude_mcp_path, get_claude_mcp_status, get_claude_settings_path,
|
||||
get_claude_mcp_path, get_claude_mcp_status, get_claude_settings_path, get_grok_config_path,
|
||||
import_default_config_test_hook, read_claude_mcp_config, update_settings, AppError,
|
||||
AppSettings, AppType, McpApps, McpServer, McpService, MultiAppConfig,
|
||||
AppSettings, AppType, McpApps, McpServer, McpService, MultiAppConfig, ProviderService,
|
||||
};
|
||||
|
||||
#[path = "support.rs"]
|
||||
@@ -68,6 +68,185 @@ fn import_default_config_claude_persists_provider() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_default_config_grokbuild_seeds_official_alongside_default() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let _home = ensure_test_home();
|
||||
|
||||
let config_path = get_grok_config_path();
|
||||
if let Some(parent) = config_path.parent() {
|
||||
fs::create_dir_all(parent).expect("create grok config dir");
|
||||
}
|
||||
fs::write(
|
||||
&config_path,
|
||||
r#"[models]
|
||||
default = "grok-4.5"
|
||||
|
||||
[model."grok-4.5"]
|
||||
model = "grok-4.5"
|
||||
base_url = "https://example.com/v1"
|
||||
name = "Example"
|
||||
api_key = "secret"
|
||||
api_backend = "responses"
|
||||
context_window = 500000
|
||||
"#,
|
||||
)
|
||||
.expect("seed grok config.toml");
|
||||
|
||||
let mut config = MultiAppConfig::default();
|
||||
config.ensure_app(&AppType::GrokBuild);
|
||||
let state = create_test_state_with_config(&config).expect("create test state");
|
||||
|
||||
import_default_config_test_hook(&state, AppType::GrokBuild)
|
||||
.expect("import default config succeeds");
|
||||
|
||||
let providers = state
|
||||
.db
|
||||
.get_all_providers(AppType::GrokBuild.as_str())
|
||||
.expect("get all providers");
|
||||
assert!(
|
||||
providers.get("default").is_some(),
|
||||
"live imported as default"
|
||||
);
|
||||
|
||||
// 初次导入已有配置时应同时补出官方入口(其它应用靠首启动主播种,
|
||||
// grokbuild 种子晚于该 flag,挂在导入动作上)
|
||||
let official = providers
|
||||
.get("grokbuild-official")
|
||||
.expect("official seed ensured alongside import");
|
||||
assert_eq!(official.category.as_deref(), Some("official"));
|
||||
|
||||
// 激活的仍是导入的原配置,官方入口只是备选
|
||||
let current_id = state
|
||||
.db
|
||||
.get_current_provider(AppType::GrokBuild.as_str())
|
||||
.expect("get current provider");
|
||||
assert_eq!(current_id.as_deref(), Some("default"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_default_config_grokbuild_official_live_imports_official_as_current() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let _home = ensure_test_home();
|
||||
|
||||
// 官方登录态的 live:无自定义模型表(允许 MCP 等其它内容)。
|
||||
// 导入的正确结果 = Grok Official 成为当前供应商,而非报错。
|
||||
let config_path = get_grok_config_path();
|
||||
if let Some(parent) = config_path.parent() {
|
||||
fs::create_dir_all(parent).expect("create grok config dir");
|
||||
}
|
||||
fs::write(&config_path, "[mcp_servers.echo]\ncommand = \"echo\"\n")
|
||||
.expect("seed official-mode grok config.toml");
|
||||
|
||||
let mut config = MultiAppConfig::default();
|
||||
config.ensure_app(&AppType::GrokBuild);
|
||||
let state = create_test_state_with_config(&config).expect("create test state");
|
||||
|
||||
let imported = import_default_config_test_hook(&state, AppType::GrokBuild)
|
||||
.expect("official-mode live imports as the official provider");
|
||||
assert!(imported, "official-mode import should report success");
|
||||
|
||||
let providers = state
|
||||
.db
|
||||
.get_all_providers(AppType::GrokBuild.as_str())
|
||||
.expect("get all providers");
|
||||
let official = providers
|
||||
.get("grokbuild-official")
|
||||
.expect("official entry ensured by import");
|
||||
assert_eq!(official.category.as_deref(), Some("official"));
|
||||
assert!(
|
||||
providers.get("default").is_none(),
|
||||
"official-mode live must not be imported as a custom default"
|
||||
);
|
||||
|
||||
let current_id = state
|
||||
.db
|
||||
.get_current_provider(AppType::GrokBuild.as_str())
|
||||
.expect("get current provider");
|
||||
assert_eq!(
|
||||
current_id.as_deref(),
|
||||
Some("grokbuild-official"),
|
||||
"official entry should become current to mirror the live state"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_import_grokbuild_official_live_does_not_resurrect_official() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let _home = ensure_test_home();
|
||||
|
||||
// 启动自动导入走 service 层(lib.rs 启动循环直接调用它):官方态 live
|
||||
// 必须报错且不产出任何条目——全项目惯例是启动自动导入只产出 default、
|
||||
// 从不产出官方条目,否则删掉的官方条目每次重启都会复活。
|
||||
// 官方态的成功导入只挂在手动导入的命令层。
|
||||
let config_path = get_grok_config_path();
|
||||
if let Some(parent) = config_path.parent() {
|
||||
fs::create_dir_all(parent).expect("create grok config dir");
|
||||
}
|
||||
fs::write(&config_path, "").expect("seed empty official-mode grok config.toml");
|
||||
|
||||
let mut config = MultiAppConfig::default();
|
||||
config.ensure_app(&AppType::GrokBuild);
|
||||
let state = create_test_state_with_config(&config).expect("create test state");
|
||||
|
||||
ProviderService::import_default_config(&state, AppType::GrokBuild)
|
||||
.expect_err("startup auto-import must not import official-mode live");
|
||||
|
||||
let providers = state
|
||||
.db
|
||||
.get_all_providers(AppType::GrokBuild.as_str())
|
||||
.expect("get all providers");
|
||||
assert!(
|
||||
providers.is_empty(),
|
||||
"startup auto-import must not create any provider from official-mode live"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_default_config_grokbuild_broken_custom_live_still_errors() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let _home = ensure_test_home();
|
||||
|
||||
// 有自定义痕迹但残缺([models] 存在、缺 [model.*]):必须报真实错误,
|
||||
// 不能被误判成官方态静默吞掉;官方入口仍由命令层前置 ensure 补出。
|
||||
let config_path = get_grok_config_path();
|
||||
if let Some(parent) = config_path.parent() {
|
||||
fs::create_dir_all(parent).expect("create grok config dir");
|
||||
}
|
||||
fs::write(&config_path, "[models]\ndefault = \"grok-4.5\"\n")
|
||||
.expect("seed broken custom grok config.toml");
|
||||
|
||||
let mut config = MultiAppConfig::default();
|
||||
config.ensure_app(&AppType::GrokBuild);
|
||||
let state = create_test_state_with_config(&config).expect("create test state");
|
||||
|
||||
import_default_config_test_hook(&state, AppType::GrokBuild)
|
||||
.expect_err("broken custom config should surface a validation error");
|
||||
|
||||
let providers = state
|
||||
.db
|
||||
.get_all_providers(AppType::GrokBuild.as_str())
|
||||
.expect("get all providers");
|
||||
assert!(
|
||||
providers.get("grokbuild-official").is_some(),
|
||||
"official entry still appears via the pre-import ensure"
|
||||
);
|
||||
assert!(providers.get("default").is_none(), "nothing was imported");
|
||||
let current_id = state
|
||||
.db
|
||||
.get_current_provider(AppType::GrokBuild.as_str())
|
||||
.expect("get current provider");
|
||||
assert_ne!(
|
||||
current_id.as_deref(),
|
||||
Some("grokbuild-official"),
|
||||
"failed import must not silently activate the official entry"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_default_config_without_live_file_returns_error() {
|
||||
use support::create_test_state;
|
||||
|
||||
Reference in New Issue
Block a user