mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
1c0ee0c58a
* feat(grokbuild): add backend integration * feat(grokbuild): add provider and management UI * test(grokbuild): cover configuration and integrations * fix(grokbuild): address backend review feedback * fix(grokbuild): complete UI review feedback * feat(grokbuild): add CLI lifecycle management * fix(grokbuild): align provider icon fallback * test(grokbuild): cover provider icon persistence
222 lines
7.0 KiB
Rust
222 lines
7.0 KiB
Rust
//! Grok Build MCP synchronization and import.
|
|
//!
|
|
//! Grok Build uses the same top-level `[mcp_servers]` TOML layout as Codex,
|
|
//! stored alongside its model configuration in `~/.grok/config.toml`.
|
|
|
|
use serde_json::{json, Value};
|
|
|
|
use crate::app_config::{McpApps, McpServer, MultiAppConfig};
|
|
use crate::error::AppError;
|
|
|
|
use super::codex::json_server_to_toml_table;
|
|
use super::validation::validate_server_spec;
|
|
|
|
fn should_sync_grokbuild_mcp() -> bool {
|
|
crate::grok_config::get_grok_config_dir().exists()
|
|
}
|
|
|
|
fn read_config_text() -> Result<String, AppError> {
|
|
let path = crate::grok_config::get_grok_config_path();
|
|
if !path.exists() {
|
|
return Ok(String::new());
|
|
}
|
|
std::fs::read_to_string(&path).map_err(|e| AppError::io(&path, e))
|
|
}
|
|
|
|
fn json_server_to_grokbuild_toml_table(server_spec: &Value) -> Result<toml_edit::Table, AppError> {
|
|
let mut table = json_server_to_toml_table(server_spec)?;
|
|
// Grok infers transport from `command` or `url` and uses `headers`, while
|
|
// Codex writes an explicit `type` plus `http_headers`.
|
|
table.remove("type");
|
|
if let Some(headers) = table.remove("http_headers") {
|
|
table.insert("headers", headers);
|
|
}
|
|
Ok(table)
|
|
}
|
|
|
|
fn toml_server_to_json(entry: &toml::value::Table) -> Value {
|
|
fn convert(value: &toml::Value) -> Option<Value> {
|
|
match value {
|
|
toml::Value::String(value) => Some(json!(value)),
|
|
toml::Value::Integer(value) => Some(json!(value)),
|
|
toml::Value::Float(value) => Some(json!(value)),
|
|
toml::Value::Boolean(value) => Some(json!(value)),
|
|
toml::Value::Datetime(value) => Some(json!(value.to_string())),
|
|
toml::Value::Array(values) => Some(Value::Array(
|
|
values.iter().filter_map(convert).collect::<Vec<_>>(),
|
|
)),
|
|
toml::Value::Table(values) => Some(Value::Object(
|
|
values
|
|
.iter()
|
|
.filter_map(|(key, value)| convert(value).map(|value| (key.clone(), value)))
|
|
.collect(),
|
|
)),
|
|
}
|
|
}
|
|
|
|
let mut spec = serde_json::Map::new();
|
|
for (key, value) in entry {
|
|
let output_key = if key == "http_headers" {
|
|
"headers"
|
|
} else {
|
|
key
|
|
};
|
|
if let Some(value) = convert(value) {
|
|
spec.insert(output_key.to_string(), value);
|
|
}
|
|
}
|
|
let default_type = if spec.contains_key("url") {
|
|
"http"
|
|
} else {
|
|
"stdio"
|
|
};
|
|
spec.entry("type".to_string())
|
|
.or_insert_with(|| json!(default_type));
|
|
Value::Object(spec)
|
|
}
|
|
|
|
pub fn import_from_grokbuild(config: &mut MultiAppConfig) -> Result<usize, AppError> {
|
|
let text = read_config_text()?;
|
|
if text.trim().is_empty() {
|
|
return Ok(0);
|
|
}
|
|
let root: toml::Table = toml::from_str(&text)
|
|
.map_err(|e| AppError::McpValidation(format!("解析 ~/.grok/config.toml 失败: {e}")))?;
|
|
let Some(entries) = root.get("mcp_servers").and_then(toml::Value::as_table) else {
|
|
return Ok(0);
|
|
};
|
|
|
|
let servers = config
|
|
.mcp
|
|
.servers
|
|
.get_or_insert_with(std::collections::HashMap::new);
|
|
let mut changed = 0;
|
|
for (id, entry) in entries {
|
|
let Some(entry) = entry.as_table() else {
|
|
continue;
|
|
};
|
|
let spec = toml_server_to_json(entry);
|
|
if let Err(error) = validate_server_spec(&spec) {
|
|
log::warn!("跳过无效 Grok Build MCP 项 '{id}': {error}");
|
|
continue;
|
|
}
|
|
|
|
if let Some(existing) = servers.get_mut(id) {
|
|
if !existing.apps.grokbuild {
|
|
existing.apps.grokbuild = true;
|
|
changed += 1;
|
|
}
|
|
} else {
|
|
servers.insert(
|
|
id.clone(),
|
|
McpServer {
|
|
id: id.clone(),
|
|
name: id.clone(),
|
|
server: spec,
|
|
apps: McpApps {
|
|
grokbuild: true,
|
|
..Default::default()
|
|
},
|
|
description: None,
|
|
homepage: None,
|
|
docs: None,
|
|
tags: Vec::new(),
|
|
},
|
|
);
|
|
changed += 1;
|
|
}
|
|
}
|
|
Ok(changed)
|
|
}
|
|
|
|
pub fn sync_single_server_to_grokbuild(
|
|
_config: &MultiAppConfig,
|
|
id: &str,
|
|
server_spec: &Value,
|
|
) -> Result<(), AppError> {
|
|
if !should_sync_grokbuild_mcp() {
|
|
return Ok(());
|
|
}
|
|
use toml_edit::Item;
|
|
|
|
let path = crate::grok_config::get_grok_config_path();
|
|
let text = read_config_text()?;
|
|
let mut doc = if text.trim().is_empty() {
|
|
toml_edit::DocumentMut::new()
|
|
} else {
|
|
text.parse::<toml_edit::DocumentMut>().map_err(|e| {
|
|
AppError::McpValidation(format!("解析 Grok Build config.toml 失败: {e}"))
|
|
})?
|
|
};
|
|
if !doc.contains_key("mcp_servers") {
|
|
doc["mcp_servers"] = toml_edit::table();
|
|
}
|
|
doc["mcp_servers"][id] = Item::Table(json_server_to_grokbuild_toml_table(server_spec)?);
|
|
crate::config::write_text_file(&path, &doc.to_string())
|
|
}
|
|
|
|
pub fn remove_server_from_grokbuild(id: &str) -> Result<(), AppError> {
|
|
if !should_sync_grokbuild_mcp() {
|
|
return Ok(());
|
|
}
|
|
let path = crate::grok_config::get_grok_config_path();
|
|
if !path.exists() {
|
|
return Ok(());
|
|
}
|
|
let text = read_config_text()?;
|
|
let mut doc = match text.parse::<toml_edit::DocumentMut>() {
|
|
Ok(doc) => doc,
|
|
Err(error) => {
|
|
log::warn!("解析 Grok Build config.toml 失败: {error},跳过删除操作");
|
|
return Ok(());
|
|
}
|
|
};
|
|
if let Some(servers) = doc
|
|
.get_mut("mcp_servers")
|
|
.and_then(toml_edit::Item::as_table_mut)
|
|
{
|
|
servers.remove(id);
|
|
}
|
|
crate::config::write_text_file(&path, &doc.to_string())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn converts_http_headers_to_unified_headers() {
|
|
let entry: toml::value::Table = toml::from_str(
|
|
r#"type = "http"
|
|
url = "https://example.com/mcp"
|
|
http_headers = { Authorization = "Bearer token" }
|
|
"#,
|
|
)
|
|
.expect("parse table");
|
|
let spec = toml_server_to_json(&entry);
|
|
assert_eq!(spec["type"], "http");
|
|
assert_eq!(spec["headers"]["Authorization"], "Bearer token");
|
|
}
|
|
|
|
#[test]
|
|
fn writes_grokbuild_remote_server_without_codex_only_fields() {
|
|
let table = json_server_to_grokbuild_toml_table(&json!({
|
|
"type": "http",
|
|
"url": "https://example.com/mcp",
|
|
"headers": { "Authorization": "Bearer token" }
|
|
}))
|
|
.expect("convert server");
|
|
|
|
assert!(!table.contains_key("type"));
|
|
assert!(!table.contains_key("http_headers"));
|
|
assert_eq!(
|
|
table
|
|
.get("headers")
|
|
.and_then(toml_edit::Item::as_table)
|
|
.and_then(|headers| headers.get("Authorization"))
|
|
.and_then(toml_edit::Item::as_str),
|
|
Some("Bearer token")
|
|
);
|
|
}
|
|
}
|