feat(agents): add Kimi Code and Grok Build integrations
This commit is contained in:
1 parent
d093a0122e
commit
49893bb32e
16 files changed
+937
-3
No files matched your search
@@ -99,6 +99,8 @@ OpenAI、Claude、Gemini、およびその他の互換形式の間で変換で
|
||||
- Hermes Agent
|
||||
- Pi(CLIProxyAPI provider 拡張機能)
|
||||
- ZCode
|
||||
- Kimi Code
|
||||
- Grok Build
|
||||
|
||||
対応クライアントでは、利用可能なモデルカタログの同期、デフォルトモデルの選択、管理設定を適用する前の
|
||||
元設定のバックアップ、以前の設定への復元、利用可能なデスクトップまたは CLI エントリーポイントの起動ができます。
|
||||
|
||||
@@ -100,6 +100,8 @@ proxy. Supported clients include:
|
||||
- Hermes Agent
|
||||
- Pi (with the CLIProxyAPI provider extension)
|
||||
- ZCode
|
||||
- Kimi Code
|
||||
- Grok Build
|
||||
|
||||
For supported clients, the application can synchronize the available model catalog, select a
|
||||
default model, back up the original configuration before applying managed settings, and restore the
|
||||
|
||||
@@ -96,6 +96,8 @@ API 接入页面按照协议或 Provider 管理上游 API 凭证和服务地址
|
||||
- Hermes Agent
|
||||
- Pi(通过 CLIProxyAPI provider 插件)
|
||||
- ZCode
|
||||
- Kimi Code
|
||||
- Grok Build
|
||||
|
||||
对于受支持的客户端,软件可以同步可用模型目录、选择默认模型、在应用托管配置前备份原始配置,
|
||||
以及恢复之前的配置。
|
||||
|
||||
@@ -18,6 +18,8 @@ pub(crate) fn inspect_agent_config_statuses(
|
||||
AgentClient::Hermes,
|
||||
AgentClient::DeepSeekHarness,
|
||||
AgentClient::ZCode,
|
||||
AgentClient::KimiCode,
|
||||
AgentClient::GrokBuild,
|
||||
]
|
||||
.into_iter()
|
||||
.map(|client| inspect_agent_config(client, &home, config.port, api_key))
|
||||
|
||||
@@ -226,6 +226,40 @@ pub(crate) fn build_agent_updates_with_oauth(
|
||||
after,
|
||||
}])
|
||||
}
|
||||
AgentClient::KimiCode => {
|
||||
let before = read_optional_text(&paths[0])?;
|
||||
let after = build_kimi_code_agent_config(
|
||||
before.as_deref(),
|
||||
&openai_base,
|
||||
api_key,
|
||||
model,
|
||||
models,
|
||||
)
|
||||
.or_else(|_| {
|
||||
build_kimi_code_agent_config(None, &openai_base, api_key, model, models)
|
||||
})?;
|
||||
Ok(vec![AgentFileUpdate {
|
||||
path: paths[0].clone(),
|
||||
after,
|
||||
}])
|
||||
}
|
||||
AgentClient::GrokBuild => {
|
||||
let before = read_optional_text(&paths[0])?;
|
||||
let after = build_grok_build_agent_config(
|
||||
before.as_deref(),
|
||||
&openai_base,
|
||||
api_key,
|
||||
model,
|
||||
models,
|
||||
)
|
||||
.or_else(|_| {
|
||||
build_grok_build_agent_config(None, &openai_base, api_key, model, models)
|
||||
})?;
|
||||
Ok(vec![AgentFileUpdate {
|
||||
path: paths[0].clone(),
|
||||
after,
|
||||
}])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1028,6 +1062,138 @@ pub(crate) fn remove_zcode_managed_configuration(paths: &[PathBuf]) -> Result<Ve
|
||||
Ok(updated.then(|| path_to_string(path)).into_iter().collect())
|
||||
}
|
||||
|
||||
pub(crate) fn remove_kimi_code_managed_configuration(
|
||||
paths: &[PathBuf],
|
||||
) -> Result<Vec<String>, String> {
|
||||
remove_managed_toml_client_configuration(
|
||||
paths,
|
||||
"Kimi Code",
|
||||
Some("providers"),
|
||||
"models",
|
||||
None,
|
||||
"default_model",
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_grok_build_managed_configuration(
|
||||
paths: &[PathBuf],
|
||||
) -> Result<Vec<String>, String> {
|
||||
remove_managed_toml_client_configuration(
|
||||
paths,
|
||||
"Grok Build",
|
||||
None,
|
||||
"model",
|
||||
Some("models"),
|
||||
"default",
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_managed_toml_client_configuration(
|
||||
paths: &[PathBuf],
|
||||
label: &str,
|
||||
provider_section: Option<&str>,
|
||||
model_entry_section: &str,
|
||||
default_section: Option<&str>,
|
||||
default_key: &str,
|
||||
) -> Result<Vec<String>, String> {
|
||||
use toml_edit::{Document, Item};
|
||||
|
||||
let Some(path) = paths.first() else {
|
||||
return Err(format!("{label} 当前平台配置路径不可用"));
|
||||
};
|
||||
if !path.is_file() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let content = fs::read_to_string(path)
|
||||
.map_err(|error| format!("读取 {label} 配置失败 {}: {error}", path_to_string(path)))?;
|
||||
let mut document = content
|
||||
.parse::<Document>()
|
||||
.map_err(|error| format!("解析 {label} 配置失败: {error}"))?;
|
||||
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
|
||||
let mut changed = false;
|
||||
|
||||
let default_table = if let Some(section) = default_section {
|
||||
document
|
||||
.as_table_mut()
|
||||
.get_mut(section)
|
||||
.and_then(Item::as_table_mut)
|
||||
} else {
|
||||
Some(document.as_table_mut())
|
||||
};
|
||||
if let Some(table) = default_table {
|
||||
if table
|
||||
.get(default_key)
|
||||
.and_then(Item::as_str)
|
||||
.is_some_and(|model| model.starts_with(&prefix))
|
||||
{
|
||||
table.remove(default_key);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(section) = provider_section {
|
||||
if let Some(providers) = document
|
||||
.as_table_mut()
|
||||
.get_mut(section)
|
||||
.and_then(Item::as_table_mut)
|
||||
{
|
||||
changed |= providers.remove(MANAGED_AGENT_PROVIDER_ID).is_some();
|
||||
}
|
||||
}
|
||||
if let Some(models) = document
|
||||
.as_table_mut()
|
||||
.get_mut(model_entry_section)
|
||||
.and_then(Item::as_table_mut)
|
||||
{
|
||||
let managed = models
|
||||
.iter()
|
||||
.map(|(key, _)| key.to_string())
|
||||
.filter(|key| key.starts_with(&prefix))
|
||||
.collect::<Vec<_>>();
|
||||
changed |= !managed.is_empty();
|
||||
for key in managed {
|
||||
models.remove(&key);
|
||||
}
|
||||
}
|
||||
for section in provider_section.into_iter().chain([model_entry_section]) {
|
||||
let empty = document
|
||||
.as_table()
|
||||
.get(section)
|
||||
.and_then(Item::as_table)
|
||||
.is_some_and(toml_edit::Table::is_empty);
|
||||
if empty {
|
||||
document.as_table_mut().remove(section);
|
||||
}
|
||||
}
|
||||
if let Some(section) = default_section {
|
||||
let empty = document
|
||||
.as_table()
|
||||
.get(section)
|
||||
.and_then(Item::as_table)
|
||||
.is_some_and(toml_edit::Table::is_empty);
|
||||
if empty {
|
||||
document.as_table_mut().remove(section);
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let rendered = document.to_string();
|
||||
toml::from_str::<toml::Value>(&rendered)
|
||||
.map_err(|error| format!("验证恢复后的 {label} 配置失败: {error}"))?;
|
||||
if rendered.trim().is_empty() {
|
||||
fs::remove_file(path).map_err(|error| {
|
||||
format!(
|
||||
"删除空的 {label} 配置失败 {}: {error}",
|
||||
path_to_string(path)
|
||||
)
|
||||
})?;
|
||||
} else {
|
||||
write_bytes_directly(path, rendered.as_bytes())?;
|
||||
}
|
||||
Ok(vec![path_to_string(path)])
|
||||
}
|
||||
|
||||
pub(crate) fn update_agent_json5_file<F>(
|
||||
path: &Path,
|
||||
label: &str,
|
||||
@@ -1219,6 +1385,8 @@ pub(crate) fn remove_agent_managed_configuration(
|
||||
AgentClient::Hermes => remove_hermes_managed_configuration(paths),
|
||||
AgentClient::DeepSeekHarness => remove_deepseek_harness_managed_configuration(paths),
|
||||
AgentClient::ZCode => remove_zcode_managed_configuration(paths),
|
||||
AgentClient::KimiCode => remove_kimi_code_managed_configuration(paths),
|
||||
AgentClient::GrokBuild => remove_grok_build_managed_configuration(paths),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1601,6 +1769,168 @@ pub(crate) fn build_restored_zcode_config(
|
||||
render_restored_json(root, original.is_some(), "恢复后的 ZCode 配置")
|
||||
}
|
||||
|
||||
pub(crate) fn build_restored_kimi_code_config(
|
||||
current: &str,
|
||||
original: Option<&str>,
|
||||
) -> Result<Option<String>, String> {
|
||||
build_restored_managed_toml_client_config(
|
||||
current,
|
||||
original,
|
||||
"Kimi Code",
|
||||
Some(("providers", &["type", "base_url", "api_key"])),
|
||||
"models",
|
||||
None,
|
||||
"default_model",
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn build_restored_grok_build_config(
|
||||
current: &str,
|
||||
original: Option<&str>,
|
||||
) -> Result<Option<String>, String> {
|
||||
build_restored_managed_toml_client_config(
|
||||
current,
|
||||
original,
|
||||
"Grok Build",
|
||||
None,
|
||||
"model",
|
||||
Some("models"),
|
||||
"default",
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn build_restored_managed_toml_client_config(
|
||||
current: &str,
|
||||
original: Option<&str>,
|
||||
label: &str,
|
||||
provider: Option<(&str, &[&str])>,
|
||||
model_entry_section: &str,
|
||||
default_section: Option<&str>,
|
||||
default_key: &str,
|
||||
) -> Result<Option<String>, String> {
|
||||
use toml_edit::Item;
|
||||
|
||||
let mut current_document = parse_codex_document(Some(current), &format!("当前 {label} 配置"))?;
|
||||
let original_document = original
|
||||
.map(|content| parse_codex_document(Some(content), &format!("原始 {label} 配置")))
|
||||
.transpose()?;
|
||||
if let Some(original_document) = original_document.as_ref() {
|
||||
merge_missing_codex_table_items(
|
||||
current_document.as_table_mut(),
|
||||
original_document.as_table(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(section) = default_section {
|
||||
let original_table = original_document
|
||||
.as_ref()
|
||||
.and_then(|document| document.as_table().get(section))
|
||||
.and_then(Item::as_table);
|
||||
if let Some(current_table) = current_document
|
||||
.as_table_mut()
|
||||
.get_mut(section)
|
||||
.and_then(Item::as_table_mut)
|
||||
{
|
||||
restore_codex_table_item(current_table, original_table, default_key);
|
||||
}
|
||||
} else {
|
||||
restore_codex_table_item(
|
||||
current_document.as_table_mut(),
|
||||
original_document
|
||||
.as_ref()
|
||||
.map(|document| document.as_table()),
|
||||
default_key,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some((section, managed_keys)) = provider {
|
||||
let original_provider = original_document
|
||||
.as_ref()
|
||||
.and_then(|document| document.as_table().get(section))
|
||||
.and_then(Item::as_table)
|
||||
.and_then(|providers| providers.get(MANAGED_AGENT_PROVIDER_ID))
|
||||
.and_then(Item::as_table)
|
||||
.cloned();
|
||||
if let Some(providers) = current_document
|
||||
.as_table_mut()
|
||||
.get_mut(section)
|
||||
.and_then(Item::as_table_mut)
|
||||
{
|
||||
if let Some(current_provider) = providers
|
||||
.get_mut(MANAGED_AGENT_PROVIDER_ID)
|
||||
.and_then(Item::as_table_mut)
|
||||
{
|
||||
for key in managed_keys {
|
||||
restore_codex_table_item(current_provider, original_provider.as_ref(), key);
|
||||
}
|
||||
if current_provider.is_empty() {
|
||||
providers.remove(MANAGED_AGENT_PROVIDER_ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
|
||||
let original_models = original_document
|
||||
.as_ref()
|
||||
.and_then(|document| document.as_table().get(model_entry_section))
|
||||
.and_then(Item::as_table)
|
||||
.map(|models| {
|
||||
models
|
||||
.iter()
|
||||
.filter(|(key, _)| key.starts_with(&prefix))
|
||||
.map(|(key, item)| (key.to_string(), item.clone()))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if let Some(models) = current_document
|
||||
.as_table_mut()
|
||||
.get_mut(model_entry_section)
|
||||
.and_then(Item::as_table_mut)
|
||||
{
|
||||
let managed = models
|
||||
.iter()
|
||||
.map(|(key, _)| key.to_string())
|
||||
.filter(|key| key.starts_with(&prefix))
|
||||
.collect::<Vec<_>>();
|
||||
for key in managed {
|
||||
models.remove(&key);
|
||||
}
|
||||
for (key, item) in &original_models {
|
||||
models.insert(key, item.clone());
|
||||
}
|
||||
} else if !original_models.is_empty() {
|
||||
let models = ensure_toml_child_table(current_document.as_table_mut(), model_entry_section);
|
||||
for (key, item) in &original_models {
|
||||
models.insert(key, item.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for section in provider
|
||||
.map(|(section, _)| section)
|
||||
.into_iter()
|
||||
.chain([model_entry_section])
|
||||
.chain(default_section)
|
||||
{
|
||||
let empty = current_document
|
||||
.as_table()
|
||||
.get(section)
|
||||
.and_then(Item::as_table)
|
||||
.is_some_and(toml_edit::Table::is_empty);
|
||||
if empty {
|
||||
current_document.as_table_mut().remove(section);
|
||||
}
|
||||
}
|
||||
let rendered = current_document.to_string();
|
||||
toml::from_str::<toml::Value>(&rendered)
|
||||
.map_err(|error| format!("验证恢复后的 {label} 配置失败: {error}"))?;
|
||||
if original.is_none() && rendered.trim().is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(rendered))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_restored_openclaw_config(
|
||||
current: &str,
|
||||
original: Option<&str>,
|
||||
@@ -1962,7 +2292,7 @@ pub(crate) fn agent_config_semantically_equal(
|
||||
expected: &str,
|
||||
) -> bool {
|
||||
match client {
|
||||
AgentClient::Codex => {
|
||||
AgentClient::Codex | AgentClient::KimiCode | AgentClient::GrokBuild => {
|
||||
toml::from_str::<toml::Value>(actual).ok()
|
||||
== toml::from_str::<toml::Value>(expected).ok()
|
||||
}
|
||||
@@ -2032,6 +2362,8 @@ pub(crate) fn build_agent_session_restored_bytes(
|
||||
}
|
||||
}
|
||||
AgentClient::ZCode => build_restored_zcode_config(current, original)?,
|
||||
AgentClient::KimiCode => build_restored_kimi_code_config(current, original)?,
|
||||
AgentClient::GrokBuild => build_restored_grok_build_config(current, original)?,
|
||||
};
|
||||
if let (Some(restored), Some(original), Some(original_bytes)) =
|
||||
(restored.as_deref(), original, original_bytes)
|
||||
@@ -2413,6 +2745,128 @@ pub(crate) fn build_zcode_agent_config(
|
||||
render_agent_json(root, "ZCode 配置")
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_toml_child_table<'a>(
|
||||
parent: &'a mut toml_edit::Table,
|
||||
key: &str,
|
||||
) -> &'a mut toml_edit::Table {
|
||||
use toml_edit::{Item, Table};
|
||||
|
||||
if !parent.get(key).is_some_and(Item::is_table) {
|
||||
parent.remove(key);
|
||||
parent.insert(key, Item::Table(Table::new()));
|
||||
}
|
||||
parent
|
||||
.get_mut(key)
|
||||
.and_then(Item::as_table_mut)
|
||||
.expect("inserted TOML table must remain a table")
|
||||
}
|
||||
|
||||
pub(crate) fn managed_model_alias(model: &str) -> String {
|
||||
format!("{MANAGED_AGENT_PROVIDER_ID}/{model}")
|
||||
}
|
||||
|
||||
pub(crate) fn build_kimi_code_agent_config(
|
||||
existing: Option<&str>,
|
||||
base_url: &str,
|
||||
api_key: &str,
|
||||
model: &str,
|
||||
available_models: &[AgentModelOption],
|
||||
) -> Result<String, String> {
|
||||
use toml_edit::{value, Array, Item, Table};
|
||||
|
||||
let mut document = parse_codex_document(existing, "Kimi Code config.toml")?;
|
||||
set_codex_table_item(
|
||||
document.as_table_mut(),
|
||||
"default_model",
|
||||
value(managed_model_alias(model)),
|
||||
);
|
||||
|
||||
let providers = ensure_toml_child_table(document.as_table_mut(), "providers");
|
||||
let provider = ensure_toml_child_table(providers, MANAGED_AGENT_PROVIDER_ID);
|
||||
set_codex_table_item(provider, "type", value("openai"));
|
||||
set_codex_table_item(provider, "base_url", value(base_url));
|
||||
set_codex_table_item(provider, "api_key", value(api_key));
|
||||
|
||||
let models = ensure_toml_child_table(document.as_table_mut(), "models");
|
||||
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
|
||||
let stale = models
|
||||
.iter()
|
||||
.map(|(key, _)| key.to_string())
|
||||
.filter(|key| key.starts_with(&prefix))
|
||||
.collect::<Vec<_>>();
|
||||
for key in stale {
|
||||
models.remove(&key);
|
||||
}
|
||||
for option in ordered_agent_models(available_models, model) {
|
||||
let mut entry = Table::new();
|
||||
entry.insert("provider", value(MANAGED_AGENT_PROVIDER_ID));
|
||||
entry.insert("model", value(option.name.clone()));
|
||||
entry.insert(
|
||||
"display_name",
|
||||
value(option.alias.as_deref().unwrap_or(&option.name)),
|
||||
);
|
||||
let context = option
|
||||
.context_window
|
||||
.unwrap_or(200_000)
|
||||
.min(i64::MAX as u64) as i64;
|
||||
entry.insert("max_context_size", value(context));
|
||||
let mut capabilities = Array::new();
|
||||
capabilities.push("tool_use");
|
||||
entry.insert("capabilities", value(capabilities));
|
||||
models.insert(&managed_model_alias(&option.name), Item::Table(entry));
|
||||
}
|
||||
let rendered = document.to_string();
|
||||
toml::from_str::<toml::Value>(&rendered)
|
||||
.map_err(|error| format!("验证 Kimi Code 配置失败: {error}"))?;
|
||||
Ok(rendered)
|
||||
}
|
||||
|
||||
pub(crate) fn build_grok_build_agent_config(
|
||||
existing: Option<&str>,
|
||||
base_url: &str,
|
||||
api_key: &str,
|
||||
model: &str,
|
||||
available_models: &[AgentModelOption],
|
||||
) -> Result<String, String> {
|
||||
use toml_edit::{value, Item, Table};
|
||||
|
||||
let mut document = parse_codex_document(existing, "Grok Build config.toml")?;
|
||||
let defaults = ensure_toml_child_table(document.as_table_mut(), "models");
|
||||
set_codex_table_item(defaults, "default", value(managed_model_alias(model)));
|
||||
|
||||
let models = ensure_toml_child_table(document.as_table_mut(), "model");
|
||||
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
|
||||
let stale = models
|
||||
.iter()
|
||||
.map(|(key, _)| key.to_string())
|
||||
.filter(|key| key.starts_with(&prefix))
|
||||
.collect::<Vec<_>>();
|
||||
for key in stale {
|
||||
models.remove(&key);
|
||||
}
|
||||
for option in ordered_agent_models(available_models, model) {
|
||||
let mut entry = Table::new();
|
||||
entry.insert("model", value(option.name.clone()));
|
||||
entry.insert("base_url", value(base_url));
|
||||
entry.insert(
|
||||
"name",
|
||||
value(option.alias.as_deref().unwrap_or(&option.name)),
|
||||
);
|
||||
entry.insert("api_key", value(api_key));
|
||||
entry.insert("api_backend", value("chat_completions"));
|
||||
let context = option
|
||||
.context_window
|
||||
.unwrap_or(200_000)
|
||||
.min(i64::MAX as u64) as i64;
|
||||
entry.insert("context_window", value(context));
|
||||
models.insert(&managed_model_alias(&option.name), Item::Table(entry));
|
||||
}
|
||||
let rendered = document.to_string();
|
||||
toml::from_str::<toml::Value>(&rendered)
|
||||
.map_err(|error| format!("验证 Grok Build 配置失败: {error}"))?;
|
||||
Ok(rendered)
|
||||
}
|
||||
|
||||
pub(crate) fn build_openclaw_agent_config(
|
||||
existing: Option<&str>,
|
||||
base_url: &str,
|
||||
|
||||
@@ -33,9 +33,25 @@ pub(crate) fn agent_config_paths(client: AgentClient, home: &Path) -> Vec<PathBu
|
||||
]
|
||||
}
|
||||
AgentClient::ZCode => vec![home.join(".zcode/v2").join(ZCODE_CONFIG_FILE)],
|
||||
AgentClient::KimiCode => vec![kimi_code_home(home).join(KIMI_CODE_CONFIG_FILE)],
|
||||
AgentClient::GrokBuild => vec![grok_build_home(home).join(GROK_BUILD_CONFIG_FILE)],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn kimi_code_home(home: &Path) -> PathBuf {
|
||||
env::var_os("KIMI_CODE_HOME")
|
||||
.map(PathBuf::from)
|
||||
.filter(|path| !path.as_os_str().is_empty())
|
||||
.unwrap_or_else(|| home.join(".kimi-code"))
|
||||
}
|
||||
|
||||
pub(crate) fn grok_build_home(home: &Path) -> PathBuf {
|
||||
env::var_os("GROK_HOME")
|
||||
.map(PathBuf::from)
|
||||
.filter(|path| !path.as_os_str().is_empty())
|
||||
.unwrap_or_else(|| home.join(".grok"))
|
||||
}
|
||||
|
||||
pub(crate) fn deepseek_harness_home(home: &Path) -> PathBuf {
|
||||
env::var_os("DSH_HOME")
|
||||
.map(PathBuf::from)
|
||||
@@ -954,6 +970,10 @@ pub(crate) fn inspect_agent_managed_config(
|
||||
.map(|(configured, model)| (configured, model, false)),
|
||||
AgentClient::ZCode => inspect_zcode_agent_config(&paths[0], port, api_key)
|
||||
.map(|(configured, model)| (configured, model, false)),
|
||||
AgentClient::KimiCode => inspect_kimi_code_agent_config(&paths[0], port, api_key)
|
||||
.map(|(configured, model)| (configured, model, false)),
|
||||
AgentClient::GrokBuild => inspect_grok_build_agent_config(&paths[0], port, api_key)
|
||||
.map(|(configured, model)| (configured, model, false)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1089,6 +1109,22 @@ pub(crate) fn agent_has_managed_marker(
|
||||
.is_some_and(|model| model.starts_with(&prefix));
|
||||
Ok(provider_exists && model_selected)
|
||||
}
|
||||
AgentClient::KimiCode => inspect_managed_toml_model_marker(
|
||||
&paths[0],
|
||||
"Kimi Code 配置",
|
||||
Some("providers"),
|
||||
"models",
|
||||
None,
|
||||
"default_model",
|
||||
),
|
||||
AgentClient::GrokBuild => inspect_managed_toml_model_marker(
|
||||
&paths[0],
|
||||
"Grok Build 配置",
|
||||
None,
|
||||
"model",
|
||||
Some("models"),
|
||||
"default",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1097,6 +1133,44 @@ pub(crate) fn is_managed_agent_base_url(value: &str) -> bool {
|
||||
value.starts_with("http://127.0.0.1:") || value.starts_with("http://localhost:")
|
||||
}
|
||||
|
||||
pub(crate) fn inspect_managed_toml_model_marker(
|
||||
path: &Path,
|
||||
label: &str,
|
||||
provider_section: Option<&str>,
|
||||
model_entry_section: &str,
|
||||
default_section: Option<&str>,
|
||||
default_key: &str,
|
||||
) -> Result<bool, String> {
|
||||
if !path.is_file() {
|
||||
return Ok(false);
|
||||
}
|
||||
let root: toml::Value = toml::from_str(
|
||||
&fs::read_to_string(path).map_err(|error| format!("读取 {label} 失败: {error}"))?,
|
||||
)
|
||||
.map_err(|error| format!("解析 {label} 失败: {error}"))?;
|
||||
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
|
||||
let selected = (if let Some(section) = default_section {
|
||||
root.get(section)
|
||||
.and_then(toml::Value::as_table)
|
||||
.and_then(|section| section.get(default_key))
|
||||
} else {
|
||||
root.get(default_key)
|
||||
})
|
||||
.and_then(toml::Value::as_str)
|
||||
.filter(|model| model.starts_with(&prefix));
|
||||
let catalog_has_selected = selected.is_some_and(|model| {
|
||||
root.get(model_entry_section)
|
||||
.and_then(toml::Value::as_table)
|
||||
.is_some_and(|catalog| catalog.contains_key(model))
|
||||
});
|
||||
let provider_exists = provider_section.is_none_or(|section| {
|
||||
root.get(section)
|
||||
.and_then(toml::Value::as_table)
|
||||
.is_some_and(|providers| providers.contains_key(MANAGED_AGENT_PROVIDER_ID))
|
||||
});
|
||||
Ok(provider_exists && catalog_has_selected)
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
pub(crate) fn find_codex_app_installation(home: &Path) -> Option<CodexAppTarget> {
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -1652,9 +1726,39 @@ pub(crate) fn find_agent_executable(client: AgentClient, home: &Path) -> Option<
|
||||
if client == AgentClient::ZCode {
|
||||
return find_zcode_desktop_executable(home);
|
||||
}
|
||||
if client == AgentClient::KimiCode {
|
||||
return find_kimi_code_executable(home);
|
||||
}
|
||||
if client == AgentClient::GrokBuild {
|
||||
return find_grok_build_executable(home);
|
||||
}
|
||||
find_named_agent_executable(home, client.executable_names())
|
||||
}
|
||||
|
||||
pub(crate) fn find_kimi_code_executable(home: &Path) -> Option<PathBuf> {
|
||||
let directory = kimi_code_home(home).join("bin");
|
||||
#[cfg(target_os = "windows")]
|
||||
let managed = [directory.join("kimi.exe"), directory.join("kimi.cmd")];
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let managed = [directory.join("kimi")];
|
||||
managed
|
||||
.into_iter()
|
||||
.find(|path| path.is_file())
|
||||
.or_else(|| find_named_agent_executable(home, &["kimi"]))
|
||||
}
|
||||
|
||||
pub(crate) fn find_grok_build_executable(home: &Path) -> Option<PathBuf> {
|
||||
let directory = grok_build_home(home).join("bin");
|
||||
#[cfg(target_os = "windows")]
|
||||
let managed = [directory.join("grok.exe"), directory.join("grok.cmd")];
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let managed = [directory.join("grok")];
|
||||
managed
|
||||
.into_iter()
|
||||
.find(|path| path.is_file())
|
||||
.or_else(|| find_named_agent_executable(home, &["grok"]))
|
||||
}
|
||||
|
||||
pub(crate) fn find_pi_executable(home: &Path) -> Option<PathBuf> {
|
||||
find_named_agent_executable(home, &["pi"])
|
||||
}
|
||||
@@ -2393,6 +2497,113 @@ pub(crate) fn inspect_zcode_agent_config(
|
||||
Ok((configured, model))
|
||||
}
|
||||
|
||||
pub(crate) fn inspect_kimi_code_agent_config(
|
||||
path: &Path,
|
||||
port: u16,
|
||||
api_key: &str,
|
||||
) -> Result<(bool, Option<String>), String> {
|
||||
if !path.is_file() {
|
||||
return Ok((false, None));
|
||||
}
|
||||
let root: toml::Value = toml::from_str(
|
||||
&fs::read_to_string(path).map_err(|error| format!("读取 Kimi Code 配置失败: {error}"))?,
|
||||
)
|
||||
.map_err(|error| format!("解析 Kimi Code 配置失败: {error}"))?;
|
||||
let expected_base = format!("http://127.0.0.1:{port}/v1");
|
||||
let provider = root
|
||||
.get("providers")
|
||||
.and_then(toml::Value::as_table)
|
||||
.and_then(|providers| providers.get(MANAGED_AGENT_PROVIDER_ID))
|
||||
.and_then(toml::Value::as_table);
|
||||
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
|
||||
let selected = root
|
||||
.get("default_model")
|
||||
.and_then(toml::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| value.starts_with(&prefix));
|
||||
let model_entry = selected.and_then(|alias| {
|
||||
root.get("models")
|
||||
.and_then(toml::Value::as_table)
|
||||
.and_then(|models| models.get(alias))
|
||||
.and_then(toml::Value::as_table)
|
||||
});
|
||||
let model = model_entry
|
||||
.and_then(|entry| entry.get("model"))
|
||||
.and_then(toml::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string);
|
||||
let configured = selected.is_some()
|
||||
&& model.is_some()
|
||||
&& provider
|
||||
.and_then(|provider| provider.get("type"))
|
||||
.and_then(toml::Value::as_str)
|
||||
== Some("openai")
|
||||
&& provider
|
||||
.and_then(|provider| provider.get("base_url"))
|
||||
.and_then(toml::Value::as_str)
|
||||
== Some(expected_base.as_str())
|
||||
&& provider
|
||||
.and_then(|provider| provider.get("api_key"))
|
||||
.and_then(toml::Value::as_str)
|
||||
== Some(api_key)
|
||||
&& model_entry
|
||||
.and_then(|entry| entry.get("provider"))
|
||||
.and_then(toml::Value::as_str)
|
||||
== Some(MANAGED_AGENT_PROVIDER_ID);
|
||||
Ok((configured, model))
|
||||
}
|
||||
|
||||
pub(crate) fn inspect_grok_build_agent_config(
|
||||
path: &Path,
|
||||
port: u16,
|
||||
api_key: &str,
|
||||
) -> Result<(bool, Option<String>), String> {
|
||||
if !path.is_file() {
|
||||
return Ok((false, None));
|
||||
}
|
||||
let root: toml::Value = toml::from_str(
|
||||
&fs::read_to_string(path).map_err(|error| format!("读取 Grok Build 配置失败: {error}"))?,
|
||||
)
|
||||
.map_err(|error| format!("解析 Grok Build 配置失败: {error}"))?;
|
||||
let expected_base = format!("http://127.0.0.1:{port}/v1");
|
||||
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
|
||||
let selected = root
|
||||
.get("models")
|
||||
.and_then(toml::Value::as_table)
|
||||
.and_then(|models| models.get("default"))
|
||||
.and_then(toml::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| value.starts_with(&prefix));
|
||||
let model_entry = selected.and_then(|alias| {
|
||||
root.get("model")
|
||||
.and_then(toml::Value::as_table)
|
||||
.and_then(|models| models.get(alias))
|
||||
.and_then(toml::Value::as_table)
|
||||
});
|
||||
let model = model_entry
|
||||
.and_then(|entry| entry.get("model"))
|
||||
.and_then(toml::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string);
|
||||
let configured = selected.is_some()
|
||||
&& model.is_some()
|
||||
&& model_entry
|
||||
.and_then(|entry| entry.get("base_url"))
|
||||
.and_then(toml::Value::as_str)
|
||||
== Some(expected_base.as_str())
|
||||
&& model_entry
|
||||
.and_then(|entry| entry.get("api_key"))
|
||||
.and_then(toml::Value::as_str)
|
||||
== Some(api_key)
|
||||
&& model_entry
|
||||
.and_then(|entry| entry.get("api_backend"))
|
||||
.and_then(toml::Value::as_str)
|
||||
== Some("chat_completions");
|
||||
Ok((configured, model))
|
||||
}
|
||||
|
||||
pub(crate) fn inspect_openclaw_agent_config(
|
||||
path: &Path,
|
||||
port: u16,
|
||||
|
||||
@@ -731,6 +731,20 @@ pub(crate) fn fresh_agent_contents_with_oauth(
|
||||
AgentClient::ZCode => Ok(vec![build_zcode_agent_config(
|
||||
None, &root_base, api_key, model, models,
|
||||
)?]),
|
||||
AgentClient::KimiCode => Ok(vec![build_kimi_code_agent_config(
|
||||
None,
|
||||
&openai_base,
|
||||
api_key,
|
||||
model,
|
||||
models,
|
||||
)?]),
|
||||
AgentClient::GrokBuild => Ok(vec![build_grok_build_agent_config(
|
||||
None,
|
||||
&openai_base,
|
||||
api_key,
|
||||
model,
|
||||
models,
|
||||
)?]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -741,6 +755,10 @@ pub(crate) fn agent_contents_equal(client: AgentClient, actual: &str, expected:
|
||||
normalize_codex_config_for_legacy_compare(actual)
|
||||
== normalize_codex_config_for_legacy_compare(expected)
|
||||
}
|
||||
AgentClient::KimiCode | AgentClient::GrokBuild => {
|
||||
toml::from_str::<toml::Value>(actual).ok()
|
||||
== toml::from_str::<toml::Value>(expected).ok()
|
||||
}
|
||||
AgentClient::OpenClaw => {
|
||||
json5::from_str::<serde_json::Value>(actual).ok()
|
||||
== json5::from_str::<serde_json::Value>(expected).ok()
|
||||
@@ -1360,13 +1378,16 @@ pub(crate) fn restore_agent_applied_state_configuration(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn agent_clients_restored_on_exit() -> [AgentClient; 5] {
|
||||
pub(crate) fn agent_clients_restored_on_exit() -> [AgentClient; 8] {
|
||||
[
|
||||
AgentClient::Codex,
|
||||
AgentClient::OpenCode,
|
||||
AgentClient::OpenClaw,
|
||||
AgentClient::Hermes,
|
||||
AgentClient::DeepSeekHarness,
|
||||
AgentClient::ZCode,
|
||||
AgentClient::KimiCode,
|
||||
AgentClient::GrokBuild,
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,10 @@ fn tracked_configuration_paths(app: &tauri::AppHandle) -> Result<Vec<PathBuf>, S
|
||||
AgentClient::OpenCode,
|
||||
AgentClient::OpenClaw,
|
||||
AgentClient::Hermes,
|
||||
AgentClient::DeepSeekHarness,
|
||||
AgentClient::ZCode,
|
||||
AgentClient::KimiCode,
|
||||
AgentClient::GrokBuild,
|
||||
] {
|
||||
paths.extend(agent_managed_paths(client, &home));
|
||||
}
|
||||
|
||||
@@ -131,6 +131,8 @@ const DEFAULT_STREAMING_BOOTSTRAP_RETRIES: u32 = 0;
|
||||
const LEGACY_DEFAULT_MANAGEMENT_SECRET_KEY: &str = "123456";
|
||||
const MANAGED_AGENT_PROVIDER_ID: &str = "cpa-gui";
|
||||
const ZCODE_CONFIG_FILE: &str = "config.json";
|
||||
const KIMI_CODE_CONFIG_FILE: &str = "config.toml";
|
||||
const GROK_BUILD_CONFIG_FILE: &str = "config.toml";
|
||||
const DEEPSEEK_HARNESS_PROVIDER_ID: &str = "easy-cliproxyapi";
|
||||
const DEEPSEEK_HARNESS_CREDENTIAL: &str = "EASYCLIPROXYAPI_API_KEY";
|
||||
const DEEPSEEK_HARNESS_SETTINGS_FILE: &str = "settings.yaml";
|
||||
@@ -974,6 +976,8 @@ enum AgentClient {
|
||||
Hermes,
|
||||
DeepSeekHarness,
|
||||
ZCode,
|
||||
KimiCode,
|
||||
GrokBuild,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -995,6 +999,8 @@ impl AgentClient {
|
||||
"hermes" => Ok(Self::Hermes),
|
||||
"deepseek-harness" => Ok(Self::DeepSeekHarness),
|
||||
"zcode" => Ok(Self::ZCode),
|
||||
"kimi-code" => Ok(Self::KimiCode),
|
||||
"grok-build" => Ok(Self::GrokBuild),
|
||||
_ => Err(format!("不支持的智能体客户端: {value}")),
|
||||
}
|
||||
}
|
||||
@@ -1009,6 +1015,8 @@ impl AgentClient {
|
||||
Self::Hermes => "hermes",
|
||||
Self::DeepSeekHarness => "deepseek-harness",
|
||||
Self::ZCode => "zcode",
|
||||
Self::KimiCode => "kimi-code",
|
||||
Self::GrokBuild => "grok-build",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1022,6 +1030,8 @@ impl AgentClient {
|
||||
Self::Hermes => "Hermes Agent",
|
||||
Self::DeepSeekHarness => "DeepSeek Harness",
|
||||
Self::ZCode => "ZCode",
|
||||
Self::KimiCode => "Kimi Code",
|
||||
Self::GrokBuild => "Grok Build",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1044,6 +1054,8 @@ impl AgentClient {
|
||||
Self::Hermes => &["hermes"],
|
||||
Self::DeepSeekHarness => &["dsh"],
|
||||
Self::ZCode => &["zcode"],
|
||||
Self::KimiCode => &["kimi"],
|
||||
Self::GrokBuild => &["grok"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -661,6 +661,201 @@ fn zcode_agent_config_preserves_other_providers_and_uses_anthropic_messages() {
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kimi_code_config_preserves_existing_tables_and_uses_openai_provider() {
|
||||
let home = agent_test_home("kimi-code-agent-config");
|
||||
let path = home.join(".kimi-code/config.toml");
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
let models = test_agent_models(&["gpt-test", "deepseek-test"]);
|
||||
let rendered = build_kimi_code_agent_config(
|
||||
Some(
|
||||
r#"theme = "dark"
|
||||
|
||||
[providers.other]
|
||||
type = "openai"
|
||||
base_url = "https://example.com/v1"
|
||||
|
||||
[models."other/model"]
|
||||
provider = "other"
|
||||
model = "model"
|
||||
"#,
|
||||
),
|
||||
"http://127.0.0.1:8317/v1",
|
||||
DEFAULT_API_KEY,
|
||||
"gpt-test",
|
||||
&models,
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(&path, &rendered).unwrap();
|
||||
let value: toml::Value = toml::from_str(&rendered).unwrap();
|
||||
|
||||
assert_eq!(value["theme"].as_str(), Some("dark"));
|
||||
assert_eq!(
|
||||
value["providers"]["other"]["base_url"].as_str(),
|
||||
Some("https://example.com/v1")
|
||||
);
|
||||
assert_eq!(value["default_model"].as_str(), Some("cpa-gui/gpt-test"));
|
||||
assert_eq!(
|
||||
value["providers"][MANAGED_AGENT_PROVIDER_ID]["type"].as_str(),
|
||||
Some("openai")
|
||||
);
|
||||
assert_eq!(
|
||||
value["providers"][MANAGED_AGENT_PROVIDER_ID]["base_url"].as_str(),
|
||||
Some("http://127.0.0.1:8317/v1")
|
||||
);
|
||||
assert_eq!(
|
||||
value["models"]["cpa-gui/gpt-test"]["capabilities"]
|
||||
.as_array()
|
||||
.unwrap()[0]
|
||||
.as_str(),
|
||||
Some("tool_use")
|
||||
);
|
||||
assert_eq!(
|
||||
inspect_kimi_code_agent_config(&path, 8317, DEFAULT_API_KEY).unwrap(),
|
||||
(true, Some("gpt-test".to_string()))
|
||||
);
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grok_build_config_preserves_existing_models_and_uses_chat_completions() {
|
||||
let home = agent_test_home("grok-build-agent-config");
|
||||
let path = home.join(".grok/config.toml");
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
let models = test_agent_models(&["gpt-test", "deepseek-test"]);
|
||||
let rendered = build_grok_build_agent_config(
|
||||
Some(
|
||||
r#"theme = "dark"
|
||||
|
||||
[models]
|
||||
default = "other"
|
||||
|
||||
[model.other]
|
||||
model = "other-model"
|
||||
base_url = "https://example.com/v1"
|
||||
api_key = "other-key"
|
||||
"#,
|
||||
),
|
||||
"http://127.0.0.1:8317/v1",
|
||||
DEFAULT_API_KEY,
|
||||
"gpt-test",
|
||||
&models,
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(&path, &rendered).unwrap();
|
||||
let value: toml::Value = toml::from_str(&rendered).unwrap();
|
||||
|
||||
assert_eq!(value["theme"].as_str(), Some("dark"));
|
||||
assert_eq!(
|
||||
value["model"]["other"]["model"].as_str(),
|
||||
Some("other-model")
|
||||
);
|
||||
assert_eq!(
|
||||
value["models"]["default"].as_str(),
|
||||
Some("cpa-gui/gpt-test")
|
||||
);
|
||||
assert_eq!(
|
||||
value["model"]["cpa-gui/gpt-test"]["base_url"].as_str(),
|
||||
Some("http://127.0.0.1:8317/v1")
|
||||
);
|
||||
assert_eq!(
|
||||
value["model"]["cpa-gui/gpt-test"]["api_backend"].as_str(),
|
||||
Some("chat_completions")
|
||||
);
|
||||
assert_eq!(
|
||||
inspect_grok_build_agent_config(&path, 8317, DEFAULT_API_KEY).unwrap(),
|
||||
(true, Some("gpt-test".to_string()))
|
||||
);
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kimi_and_grok_toml_restore_preserves_runtime_fields_and_original_selection() {
|
||||
use toml_edit::{value, Document};
|
||||
|
||||
let models = test_agent_models(&["gpt-test", "deepseek-test"]);
|
||||
let kimi_original = r#"default_model = "cpa-gui/original"
|
||||
|
||||
[providers.cpa-gui]
|
||||
type = "openai_responses"
|
||||
base_url = "https://original.example/v1"
|
||||
api_key = "original-key"
|
||||
|
||||
[models."cpa-gui/original"]
|
||||
provider = "cpa-gui"
|
||||
model = "original"
|
||||
"#;
|
||||
let kimi_managed = build_kimi_code_agent_config(
|
||||
Some(kimi_original),
|
||||
"http://127.0.0.1:8317/v1",
|
||||
DEFAULT_API_KEY,
|
||||
"gpt-test",
|
||||
&models,
|
||||
)
|
||||
.unwrap();
|
||||
let mut kimi_runtime = kimi_managed.parse::<Document>().unwrap();
|
||||
kimi_runtime
|
||||
.as_table_mut()
|
||||
.insert("runtime_added", value(true));
|
||||
kimi_runtime["providers"][MANAGED_AGENT_PROVIDER_ID]
|
||||
.as_table_mut()
|
||||
.unwrap()
|
||||
.insert("runtime_custom", value("keep"));
|
||||
let kimi_restored =
|
||||
build_restored_kimi_code_config(&kimi_runtime.to_string(), Some(kimi_original))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let kimi: toml::Value = toml::from_str(&kimi_restored).unwrap();
|
||||
assert_eq!(kimi["default_model"].as_str(), Some("cpa-gui/original"));
|
||||
assert_eq!(kimi["runtime_added"].as_bool(), Some(true));
|
||||
assert_eq!(
|
||||
kimi["providers"][MANAGED_AGENT_PROVIDER_ID]["type"].as_str(),
|
||||
Some("openai_responses")
|
||||
);
|
||||
assert_eq!(
|
||||
kimi["providers"][MANAGED_AGENT_PROVIDER_ID]["runtime_custom"].as_str(),
|
||||
Some("keep")
|
||||
);
|
||||
assert!(kimi["models"].get("cpa-gui/gpt-test").is_none());
|
||||
assert_eq!(
|
||||
kimi["models"]["cpa-gui/original"]["model"].as_str(),
|
||||
Some("original")
|
||||
);
|
||||
|
||||
let grok_original = r#"[models]
|
||||
default = "cpa-gui/original"
|
||||
|
||||
[model."cpa-gui/original"]
|
||||
model = "original"
|
||||
base_url = "https://original.example/v1"
|
||||
api_key = "original-key"
|
||||
"#;
|
||||
let grok_managed = build_grok_build_agent_config(
|
||||
Some(grok_original),
|
||||
"http://127.0.0.1:8317/v1",
|
||||
DEFAULT_API_KEY,
|
||||
"gpt-test",
|
||||
&models,
|
||||
)
|
||||
.unwrap();
|
||||
let mut grok_runtime = grok_managed.parse::<Document>().unwrap();
|
||||
grok_runtime
|
||||
.as_table_mut()
|
||||
.insert("runtime_added", value(true));
|
||||
let grok_restored =
|
||||
build_restored_grok_build_config(&grok_runtime.to_string(), Some(grok_original))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let grok: toml::Value = toml::from_str(&grok_restored).unwrap();
|
||||
assert_eq!(grok["models"]["default"].as_str(), Some("cpa-gui/original"));
|
||||
assert_eq!(grok["runtime_added"].as_bool(), Some(true));
|
||||
assert!(grok["model"].get("cpa-gui/gpt-test").is_none());
|
||||
assert_eq!(
|
||||
grok["model"]["cpa-gui/original"]["base_url"].as_str(),
|
||||
Some("https://original.example/v1")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openclaw_agent_config_accepts_json5_and_preserves_unknown_fields() {
|
||||
let models = test_agent_models(&["gpt-test", "deepseek-test"]);
|
||||
@@ -831,6 +1026,9 @@ fn exit_preserves_claude_client_configurations() {
|
||||
assert!(restored.contains(&AgentClient::Codex));
|
||||
assert!(restored.contains(&AgentClient::OpenCode));
|
||||
assert!(restored.contains(&AgentClient::DeepSeekHarness));
|
||||
assert!(restored.contains(&AgentClient::ZCode));
|
||||
assert!(restored.contains(&AgentClient::KimiCode));
|
||||
assert!(restored.contains(&AgentClient::GrokBuild));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -467,6 +467,8 @@ export const jaOverrides = {
|
||||
'agents.description.hermes': 'CPA カスタムプロバイダーを使用',
|
||||
'agents.description.deepseekHarness': 'Harness のネイティブ DeepSeek 設定と分離された OpenAI 互換ルートで CPA を使用',
|
||||
'agents.description.zcode': 'CPA の Anthropic Messages API と動的モデルカタログを使用',
|
||||
'agents.description.kimiCode': 'CPA の OpenAI Chat Completions API と動的モデルカタログを使用',
|
||||
'agents.description.grokBuild': 'CPA の OpenAI Chat Completions API とカスタムモデルカタログを使用',
|
||||
'agents.description.pi': 'CLIProxyAPI プロバイダーを使用して Pi CLI のモデルを動的に読み込みます',
|
||||
'agents.list.detecting': '検出中',
|
||||
'agents.list.unsupported': '現在のプラットフォームでは非対応',
|
||||
|
||||
@@ -467,6 +467,8 @@ export const en: Record<MessageKey, string> = {
|
||||
'agents.description.hermes': 'Uses the CPA custom provider',
|
||||
'agents.description.deepseekHarness': 'Uses CPA through an isolated, OpenAI-compatible Harness route',
|
||||
'agents.description.zcode': 'Uses the CPA Anthropic Messages API and dynamic model catalog',
|
||||
'agents.description.kimiCode': 'Uses the CPA OpenAI Chat Completions API and dynamic model catalog',
|
||||
'agents.description.grokBuild': 'Uses the CPA OpenAI Chat Completions API and custom model catalog',
|
||||
'agents.description.pi': 'Loads models dynamically in Pi through the CLIProxyAPI provider',
|
||||
'agents.list.detecting': 'Detecting',
|
||||
'agents.list.unsupported': 'Unsupported on this platform',
|
||||
|
||||
@@ -465,6 +465,8 @@ export const zhCN = {
|
||||
'agents.description.hermes': '使用 CPA custom provider',
|
||||
'agents.description.deepseekHarness': '通过通用 OpenAI 兼容路由使用 CPA,并与 Harness 原生 DeepSeek 配置隔离',
|
||||
'agents.description.zcode': '使用 CPA 的 Anthropic Messages 接口和动态模型目录',
|
||||
'agents.description.kimiCode': '使用 CPA 的 OpenAI Chat Completions 接口和动态模型目录',
|
||||
'agents.description.grokBuild': '使用 CPA 的 OpenAI Chat Completions 接口和自定义模型目录',
|
||||
'agents.description.pi': '使用 Pi CLI 和 CLIProxyAPI provider 动态加载模型',
|
||||
'agents.list.detecting': '正在检测',
|
||||
'agents.list.unsupported': '当前平台不支持',
|
||||
|
||||
@@ -27,6 +27,8 @@ import claudeIcon from '../assets/icons/claude.svg';
|
||||
import codexIcon from '../assets/icons/codex.svg';
|
||||
import deepseekIcon from '../assets/icons/deepseek.svg';
|
||||
import hermesIcon from '../assets/icons/hermes.png';
|
||||
import grokIcon from '../assets/icons/grok.svg';
|
||||
import kimiIcon from '../assets/icons/kimi-light.svg';
|
||||
import openclawIcon from '../assets/icons/openclaw.svg';
|
||||
import opencodeIcon from '../assets/icons/opencode.svg';
|
||||
import piIcon from '../assets/icons/pi-logo-on-light.svg';
|
||||
@@ -57,6 +59,8 @@ type AgentClientId =
|
||||
| 'hermes'
|
||||
| 'deepseek-harness'
|
||||
| 'zcode'
|
||||
| 'kimi-code'
|
||||
| 'grok-build'
|
||||
| 'pi';
|
||||
|
||||
type ClaudeModelMappingClientId = 'claude-code' | 'claude-desktop';
|
||||
@@ -171,7 +175,7 @@ type AgentDefinition = {
|
||||
name: string;
|
||||
icon?: string;
|
||||
Icon?: ComponentType<{ size?: number; 'aria-hidden'?: boolean }>;
|
||||
descriptionKey: 'agents.description.claudeCode' | 'agents.description.claudeDesktop' | 'agents.description.codex' | 'agents.description.opencode' | 'agents.description.openclaw' | 'agents.description.hermes' | 'agents.description.deepseekHarness' | 'agents.description.zcode' | 'agents.description.pi';
|
||||
descriptionKey: 'agents.description.claudeCode' | 'agents.description.claudeDesktop' | 'agents.description.codex' | 'agents.description.opencode' | 'agents.description.openclaw' | 'agents.description.hermes' | 'agents.description.deepseekHarness' | 'agents.description.zcode' | 'agents.description.kimiCode' | 'agents.description.grokBuild' | 'agents.description.pi';
|
||||
};
|
||||
|
||||
type AgentSubpageId = 'core' | 'sessions';
|
||||
@@ -231,6 +235,18 @@ const agentDefinitions: AgentDefinition[] = [
|
||||
Icon: Bot,
|
||||
descriptionKey: 'agents.description.zcode',
|
||||
},
|
||||
{
|
||||
id: 'kimi-code',
|
||||
name: 'Kimi Code',
|
||||
icon: kimiIcon,
|
||||
descriptionKey: 'agents.description.kimiCode',
|
||||
},
|
||||
{
|
||||
id: 'grok-build',
|
||||
name: 'Grok Build',
|
||||
icon: grokIcon,
|
||||
descriptionKey: 'agents.description.grokBuild',
|
||||
},
|
||||
{
|
||||
id: 'pi',
|
||||
name: 'Pi',
|
||||
|
||||
@@ -7,6 +7,8 @@ export type AgentConfigurationClientId =
|
||||
| 'hermes'
|
||||
| 'deepseek-harness'
|
||||
| 'zcode'
|
||||
| 'kimi-code'
|
||||
| 'grok-build'
|
||||
| 'pi';
|
||||
|
||||
export type AgentConfigurationModificationState = 'unconfigured' | 'applied' | 'invalid';
|
||||
|
||||
@@ -154,6 +154,8 @@ describe('agent configuration update action', () => {
|
||||
'openclaw',
|
||||
'hermes',
|
||||
'zcode',
|
||||
'kimi-code',
|
||||
'grok-build',
|
||||
])('%s updates only while its model draft differs', (client) => {
|
||||
expect(appliedConfiguration(client, 'model-b')).toBe('update');
|
||||
expect(appliedConfiguration(client, ' MODEL-A ')).toBe('close');
|
||||
|
||||
Reference in new issue
Block a user