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:
Jason
2026-07-21 15:31:27 +08:00
parent a5aa1fd82b
commit f733def452
16 changed files with 700 additions and 144 deletions
+36 -5
View File
@@ -1502,6 +1502,19 @@ impl ProxyService {
Ok((proxy_url, proxy_codex_base_url))
}
/// Grok Build live 是否具备可接管的自定义模型表。
///
/// 官方态 liveGrok 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 处于官方登录态(无自定义模型表),跳过代理接管"
);
}
}
}
_ => {}