feat(agents): add ZCode client integration
This commit is contained in:
1 parent
11b1ea23a5
commit
d093a0122e
16 files changed
+472
-4
No files matched your search
@@ -17,6 +17,7 @@ pub(crate) fn inspect_agent_config_statuses(
|
||||
AgentClient::OpenClaw,
|
||||
AgentClient::Hermes,
|
||||
AgentClient::DeepSeekHarness,
|
||||
AgentClient::ZCode,
|
||||
]
|
||||
.into_iter()
|
||||
.map(|client| inspect_agent_config(client, &home, config.port, api_key))
|
||||
|
||||
@@ -214,6 +214,18 @@ pub(crate) fn build_agent_updates_with_oauth(
|
||||
},
|
||||
])
|
||||
}
|
||||
AgentClient::ZCode => {
|
||||
let before = read_optional_text(&paths[0])?;
|
||||
let after =
|
||||
build_zcode_agent_config(before.as_deref(), &root_base, api_key, model, models)
|
||||
.or_else(|_| {
|
||||
build_zcode_agent_config(None, &root_base, api_key, model, models)
|
||||
})?;
|
||||
Ok(vec![AgentFileUpdate {
|
||||
path: paths[0].clone(),
|
||||
after,
|
||||
}])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -984,6 +996,38 @@ pub(crate) fn remove_opencode_managed_configuration(
|
||||
Ok(updated.then(|| path_to_string(path)).into_iter().collect())
|
||||
}
|
||||
|
||||
pub(crate) fn remove_zcode_managed_configuration(paths: &[PathBuf]) -> Result<Vec<String>, String> {
|
||||
let Some(path) = paths.first() else {
|
||||
return Err("ZCode 当前平台配置路径不可用".to_string());
|
||||
};
|
||||
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
|
||||
let updated = update_agent_json_file(path, "ZCode 配置", |root| {
|
||||
let mut changed = false;
|
||||
if root
|
||||
.get("model")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_some_and(|model| model.starts_with(&prefix))
|
||||
{
|
||||
root.remove("model");
|
||||
changed = true;
|
||||
}
|
||||
let providers_empty = if let Some(providers) = root
|
||||
.get_mut("provider")
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
{
|
||||
changed |= providers.remove(MANAGED_AGENT_PROVIDER_ID).is_some();
|
||||
providers.is_empty()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if providers_empty {
|
||||
root.remove("provider");
|
||||
}
|
||||
changed
|
||||
})?;
|
||||
Ok(updated.then(|| path_to_string(path)).into_iter().collect())
|
||||
}
|
||||
|
||||
pub(crate) fn update_agent_json5_file<F>(
|
||||
path: &Path,
|
||||
label: &str,
|
||||
@@ -1174,6 +1218,7 @@ pub(crate) fn remove_agent_managed_configuration(
|
||||
AgentClient::OpenClaw => remove_openclaw_managed_configuration(paths),
|
||||
AgentClient::Hermes => remove_hermes_managed_configuration(paths),
|
||||
AgentClient::DeepSeekHarness => remove_deepseek_harness_managed_configuration(paths),
|
||||
AgentClient::ZCode => remove_zcode_managed_configuration(paths),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1476,6 +1521,86 @@ pub(crate) fn build_restored_opencode_config(
|
||||
render_restored_json(root, original.is_some(), "恢复后的 OpenCode 配置")
|
||||
}
|
||||
|
||||
pub(crate) fn build_restored_zcode_config(
|
||||
current: &str,
|
||||
original: Option<&str>,
|
||||
) -> Result<Option<String>, String> {
|
||||
let mut root = parse_agent_json_object(Some(current), "当前 ZCode 配置")?;
|
||||
let original_root = parse_restored_json_object(original, "原始 ZCode 配置")?;
|
||||
restore_json_key(&mut root, original_root.as_ref(), "model");
|
||||
let original_provider = original_root
|
||||
.as_ref()
|
||||
.and_then(|root| root.get("provider"))
|
||||
.and_then(serde_json::Value::as_object);
|
||||
let original_managed = original_provider
|
||||
.and_then(|providers| providers.get(MANAGED_AGENT_PROVIDER_ID))
|
||||
.and_then(serde_json::Value::as_object);
|
||||
if root
|
||||
.get("provider")
|
||||
.is_some_and(serde_json::Value::is_object)
|
||||
{
|
||||
let providers = root
|
||||
.get_mut("provider")
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
.expect("ZCode provider was checked as an object");
|
||||
if providers
|
||||
.get(MANAGED_AGENT_PROVIDER_ID)
|
||||
.is_some_and(serde_json::Value::is_object)
|
||||
{
|
||||
let managed = providers
|
||||
.get_mut(MANAGED_AGENT_PROVIDER_ID)
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
.expect("ZCode managed provider was checked as an object");
|
||||
for key in [
|
||||
"enabled",
|
||||
"name",
|
||||
"source",
|
||||
"kind",
|
||||
"defaultKind",
|
||||
"apiFormat",
|
||||
"npm",
|
||||
"models",
|
||||
] {
|
||||
restore_json_key(managed, original_managed, key);
|
||||
}
|
||||
let original_options = original_managed
|
||||
.and_then(|managed| managed.get("options"))
|
||||
.and_then(serde_json::Value::as_object);
|
||||
if managed
|
||||
.get("options")
|
||||
.is_some_and(serde_json::Value::is_object)
|
||||
{
|
||||
let options = managed
|
||||
.get_mut("options")
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
.expect("ZCode options were checked as an object");
|
||||
for key in ["baseURL", "apiKey"] {
|
||||
restore_json_key(options, original_options, key);
|
||||
}
|
||||
if options.is_empty() && original_options.is_none() {
|
||||
managed.remove("options");
|
||||
}
|
||||
} else {
|
||||
restore_json_key(managed, original_managed, "options");
|
||||
}
|
||||
if managed.is_empty() && original_managed.is_none() {
|
||||
providers.remove(MANAGED_AGENT_PROVIDER_ID);
|
||||
}
|
||||
} else if let Some(original_managed) = original_provider
|
||||
.and_then(|providers| providers.get(MANAGED_AGENT_PROVIDER_ID))
|
||||
.cloned()
|
||||
{
|
||||
providers.insert(MANAGED_AGENT_PROVIDER_ID.to_string(), original_managed);
|
||||
}
|
||||
if providers.is_empty() && original_provider.is_none() {
|
||||
root.remove("provider");
|
||||
}
|
||||
} else {
|
||||
restore_json_key(&mut root, original_root.as_ref(), "provider");
|
||||
}
|
||||
render_restored_json(root, original.is_some(), "恢复后的 ZCode 配置")
|
||||
}
|
||||
|
||||
pub(crate) fn build_restored_openclaw_config(
|
||||
current: &str,
|
||||
original: Option<&str>,
|
||||
@@ -1906,6 +2031,7 @@ pub(crate) fn build_agent_session_restored_bytes(
|
||||
_ => return Err("DeepSeek Harness 恢复路径索引无效".to_string()),
|
||||
}
|
||||
}
|
||||
AgentClient::ZCode => build_restored_zcode_config(current, original)?,
|
||||
};
|
||||
if let (Some(restored), Some(original), Some(original_bytes)) =
|
||||
(restored.as_deref(), original, original_bytes)
|
||||
@@ -2242,6 +2368,51 @@ pub(crate) fn build_opencode_agent_config(
|
||||
render_agent_json(root, "OpenCode 配置")
|
||||
}
|
||||
|
||||
pub(crate) fn build_zcode_agent_config(
|
||||
existing: Option<&str>,
|
||||
base_url: &str,
|
||||
api_key: &str,
|
||||
model: &str,
|
||||
available_models: &[AgentModelOption],
|
||||
) -> Result<String, String> {
|
||||
let mut root = parse_agent_json_object(existing, "ZCode config.json")?;
|
||||
let providers = ensure_json_object_entry(&mut root, "provider");
|
||||
let models = ordered_agent_models(available_models, model)
|
||||
.into_iter()
|
||||
.map(|model| {
|
||||
let display_name = model.alias.as_deref().unwrap_or(&model.name).to_string();
|
||||
(
|
||||
model.name,
|
||||
serde_json::json!({
|
||||
"name": display_name,
|
||||
"kinds": ["anthropic"],
|
||||
"defaultKind": "anthropic"
|
||||
}),
|
||||
)
|
||||
})
|
||||
.collect::<serde_json::Map<_, _>>();
|
||||
let managed_provider = ensure_json_object_entry(providers, MANAGED_AGENT_PROVIDER_ID);
|
||||
managed_provider.insert("enabled".to_string(), serde_json::json!(true));
|
||||
managed_provider.insert("name".to_string(), serde_json::json!("EasyCLIProxyAPI"));
|
||||
managed_provider.insert("source".to_string(), serde_json::json!("custom"));
|
||||
managed_provider.insert("kind".to_string(), serde_json::json!("anthropic"));
|
||||
managed_provider.insert("defaultKind".to_string(), serde_json::json!("anthropic"));
|
||||
managed_provider.insert(
|
||||
"apiFormat".to_string(),
|
||||
serde_json::json!("anthropic-messages"),
|
||||
);
|
||||
managed_provider.insert("npm".to_string(), serde_json::json!("@ai-sdk/anthropic"));
|
||||
let options = ensure_json_object_entry(managed_provider, "options");
|
||||
options.insert("baseURL".to_string(), serde_json::json!(base_url));
|
||||
options.insert("apiKey".to_string(), serde_json::json!(api_key));
|
||||
managed_provider.insert("models".to_string(), serde_json::Value::Object(models));
|
||||
root.insert(
|
||||
"model".to_string(),
|
||||
serde_json::json!(format!("{MANAGED_AGENT_PROVIDER_ID}/{model}")),
|
||||
);
|
||||
render_agent_json(root, "ZCode 配置")
|
||||
}
|
||||
|
||||
pub(crate) fn build_openclaw_agent_config(
|
||||
existing: Option<&str>,
|
||||
base_url: &str,
|
||||
|
||||
@@ -32,6 +32,7 @@ pub(crate) fn agent_config_paths(client: AgentClient, home: &Path) -> Vec<PathBu
|
||||
directory.join(DEEPSEEK_HARNESS_CREDENTIALS_FILE),
|
||||
]
|
||||
}
|
||||
AgentClient::ZCode => vec![home.join(".zcode/v2").join(ZCODE_CONFIG_FILE)],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,15 +797,26 @@ pub(crate) fn inspect_agent_config(
|
||||
Err(error) => (false, None, false, false, Some(error)),
|
||||
};
|
||||
let executable = find_agent_executable(client, home);
|
||||
let cli_version = executable.as_deref().and_then(read_agent_version);
|
||||
// ZCode's desktop executable is an Electron app, not a CLI. Invoking it
|
||||
// with --version can start the GUI and block discovery, so only probe a
|
||||
// separately installed command-line entry point here.
|
||||
let cli_version = if client == AgentClient::ZCode {
|
||||
find_named_agent_executable(home, &["zcode"])
|
||||
.filter(|path| executable.as_ref() != Some(path))
|
||||
.as_deref()
|
||||
.and_then(read_agent_version)
|
||||
} else {
|
||||
executable.as_deref().and_then(read_agent_version)
|
||||
};
|
||||
let app_version = match client {
|
||||
AgentClient::ClaudeDesktop => read_claude_desktop_version(home),
|
||||
AgentClient::Codex => read_codex_app_version(home),
|
||||
AgentClient::DeepSeekHarness => read_deepseek_harness_profile_version(home),
|
||||
AgentClient::ZCode => read_zcode_app_version(home),
|
||||
_ => None,
|
||||
};
|
||||
let version = cli_version.clone().or_else(|| app_version.clone());
|
||||
let installed = version.is_some();
|
||||
let installed = version.is_some() || (client == AgentClient::ZCode && executable.is_some());
|
||||
let mut warnings = Vec::new();
|
||||
if !client.supported_platform() {
|
||||
warnings.push("当前平台不支持 Claude Desktop 3P 配置".to_string());
|
||||
@@ -940,6 +952,8 @@ pub(crate) fn inspect_agent_managed_config(
|
||||
.map(|(configured, model)| (configured, model, false)),
|
||||
AgentClient::DeepSeekHarness => inspect_deepseek_harness_config(paths, port, api_key)
|
||||
.map(|(configured, model)| (configured, model, false)),
|
||||
AgentClient::ZCode => inspect_zcode_agent_config(&paths[0], port, api_key)
|
||||
.map(|(configured, model)| (configured, model, false)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1059,6 +1073,22 @@ pub(crate) fn agent_has_managed_marker(
|
||||
Ok(provider_exists && model_selected)
|
||||
}
|
||||
AgentClient::DeepSeekHarness => deepseek_harness_has_managed_marker(paths),
|
||||
AgentClient::ZCode => {
|
||||
if !paths[0].is_file() {
|
||||
return Ok(false);
|
||||
}
|
||||
let root = read_agent_json_or_empty(&paths[0], "ZCode 配置")?;
|
||||
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
|
||||
let provider_exists = root
|
||||
.get("provider")
|
||||
.and_then(|value| value.get(MANAGED_AGENT_PROVIDER_ID))
|
||||
.is_some();
|
||||
let model_selected = root
|
||||
.get("model")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_some_and(|model| model.starts_with(&prefix));
|
||||
Ok(provider_exists && model_selected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1135,6 +1165,85 @@ pub(crate) fn read_codex_app_version(home: &Path) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read_zcode_app_version(home: &Path) -> Option<String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
find_zcode_desktop_executable(home).and_then(|path| {
|
||||
read_windows_executable_version(&path).or_else(|| read_agent_version(&path))
|
||||
})
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
[
|
||||
PathBuf::from("/Applications/ZCode.app"),
|
||||
home.join("Applications/ZCode.app"),
|
||||
]
|
||||
.into_iter()
|
||||
.find(|path| path.is_dir())
|
||||
.and_then(|path| read_macos_app_version(&path))
|
||||
.or_else(|| {
|
||||
find_named_agent_executable(home, &["zcode"]).and_then(|path| read_agent_version(&path))
|
||||
})
|
||||
}
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
{
|
||||
let _ = home;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn find_zcode_desktop_executable(home: &Path) -> Option<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let local = env::var_os("LOCALAPPDATA")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| home.join("AppData/Local"));
|
||||
let mut candidates = vec![
|
||||
local.join("Programs/ZCode/ZCode.exe"),
|
||||
local.join("ZCode/ZCode.exe"),
|
||||
];
|
||||
for variable in ["ProgramFiles", "ProgramFiles(x86)"] {
|
||||
if let Some(root) = env::var_os(variable)
|
||||
.map(PathBuf::from)
|
||||
.filter(|path| !path.as_os_str().is_empty())
|
||||
{
|
||||
candidates.push(root.join("ZCode/ZCode.exe"));
|
||||
}
|
||||
}
|
||||
candidates
|
||||
.into_iter()
|
||||
.find(|path| path.is_file())
|
||||
.or_else(|| find_named_agent_executable(home, &["zcode"]))
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
[
|
||||
PathBuf::from("/Applications/ZCode.app/Contents/MacOS/ZCode"),
|
||||
home.join("Applications/ZCode.app/Contents/MacOS/ZCode"),
|
||||
]
|
||||
.into_iter()
|
||||
.find(|path| path.is_file())
|
||||
.or_else(|| find_named_agent_executable(home, &["zcode"]))
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let mut candidates = agent_executable_directories(home)
|
||||
.into_iter()
|
||||
.flat_map(|directory| [directory.join("zcode"), directory.join("ZCode")])
|
||||
.collect::<Vec<_>>();
|
||||
candidates.extend([
|
||||
PathBuf::from("/opt/ZCode/zcode"),
|
||||
PathBuf::from("/opt/ZCode/ZCode"),
|
||||
PathBuf::from("/opt/zcode/zcode"),
|
||||
]);
|
||||
candidates.into_iter().find(|path| path.is_file())
|
||||
}
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
find_named_agent_executable(home, &["zcode"])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) fn read_macos_app_version(application: &Path) -> Option<String> {
|
||||
let info_plist = application.join("Contents/Info.plist");
|
||||
@@ -1540,6 +1649,9 @@ pub(crate) fn find_agent_executable(client: AgentClient, home: &Path) -> Option<
|
||||
if client == AgentClient::ClaudeDesktop {
|
||||
return find_claude_desktop_executable(home);
|
||||
}
|
||||
if client == AgentClient::ZCode {
|
||||
return find_zcode_desktop_executable(home);
|
||||
}
|
||||
find_named_agent_executable(home, client.executable_names())
|
||||
}
|
||||
|
||||
@@ -2232,6 +2344,55 @@ pub(crate) fn inspect_opencode_agent_config(
|
||||
Ok((configured, model))
|
||||
}
|
||||
|
||||
pub(crate) fn inspect_zcode_agent_config(
|
||||
path: &Path,
|
||||
port: u16,
|
||||
api_key: &str,
|
||||
) -> Result<(bool, Option<String>), String> {
|
||||
if !path.is_file() {
|
||||
return Ok((false, None));
|
||||
}
|
||||
let root: serde_json::Value = serde_json::from_str(
|
||||
&fs::read_to_string(path).map_err(|error| format!("读取 ZCode 配置失败: {error}"))?,
|
||||
)
|
||||
.map_err(|error| format!("解析 ZCode 配置失败: {error}"))?;
|
||||
let provider = root
|
||||
.get("provider")
|
||||
.and_then(|providers| providers.get(MANAGED_AGENT_PROVIDER_ID));
|
||||
let expected_base = format!("http://127.0.0.1:{port}");
|
||||
let configured = provider
|
||||
.and_then(|provider| provider.get("enabled"))
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
== Some(true)
|
||||
&& provider
|
||||
.and_then(|provider| provider.get("kind"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
== Some("anthropic")
|
||||
&& provider
|
||||
.and_then(|provider| provider.get("apiFormat"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
== Some("anthropic-messages")
|
||||
&& provider
|
||||
.and_then(|provider| provider.get("options"))
|
||||
.and_then(|options| options.get("baseURL"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
== Some(expected_base.as_str())
|
||||
&& provider
|
||||
.and_then(|provider| provider.get("options"))
|
||||
.and_then(|options| options.get("apiKey"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
== Some(api_key);
|
||||
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
|
||||
let model = root
|
||||
.get("model")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| value.strip_prefix(&prefix).unwrap_or(value))
|
||||
.map(str::to_string);
|
||||
Ok((configured, model))
|
||||
}
|
||||
|
||||
pub(crate) fn inspect_openclaw_agent_config(
|
||||
path: &Path,
|
||||
port: u16,
|
||||
|
||||
@@ -728,6 +728,9 @@ pub(crate) fn fresh_agent_contents_with_oauth(
|
||||
build_deepseek_harness_settings(None, &openai_base, model, models)?,
|
||||
build_deepseek_harness_credentials(None, api_key)?,
|
||||
]),
|
||||
AgentClient::ZCode => Ok(vec![build_zcode_agent_config(
|
||||
None, &root_base, api_key, model, models,
|
||||
)?]),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ const DEFAULT_MAX_RETRY_INTERVAL: u32 = 30;
|
||||
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 DEEPSEEK_HARNESS_PROVIDER_ID: &str = "easy-cliproxyapi";
|
||||
const DEEPSEEK_HARNESS_CREDENTIAL: &str = "EASYCLIPROXYAPI_API_KEY";
|
||||
const DEEPSEEK_HARNESS_SETTINGS_FILE: &str = "settings.yaml";
|
||||
@@ -972,6 +973,7 @@ enum AgentClient {
|
||||
OpenClaw,
|
||||
Hermes,
|
||||
DeepSeekHarness,
|
||||
ZCode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -992,6 +994,7 @@ impl AgentClient {
|
||||
"openclaw" => Ok(Self::OpenClaw),
|
||||
"hermes" => Ok(Self::Hermes),
|
||||
"deepseek-harness" => Ok(Self::DeepSeekHarness),
|
||||
"zcode" => Ok(Self::ZCode),
|
||||
_ => Err(format!("不支持的智能体客户端: {value}")),
|
||||
}
|
||||
}
|
||||
@@ -1005,6 +1008,7 @@ impl AgentClient {
|
||||
Self::OpenClaw => "openclaw",
|
||||
Self::Hermes => "hermes",
|
||||
Self::DeepSeekHarness => "deepseek-harness",
|
||||
Self::ZCode => "zcode",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1017,6 +1021,7 @@ impl AgentClient {
|
||||
Self::OpenClaw => "OpenClaw",
|
||||
Self::Hermes => "Hermes Agent",
|
||||
Self::DeepSeekHarness => "DeepSeek Harness",
|
||||
Self::ZCode => "ZCode",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1038,6 +1043,7 @@ impl AgentClient {
|
||||
Self::OpenClaw => &["openclaw"],
|
||||
Self::Hermes => &["hermes"],
|
||||
Self::DeepSeekHarness => &["dsh"],
|
||||
Self::ZCode => &["zcode"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,6 +612,55 @@ fn opencode_agent_config_preserves_other_providers() {
|
||||
assert!(value["provider"][MANAGED_AGENT_PROVIDER_ID]["models"]["deepseek-test"].is_object());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zcode_agent_config_preserves_other_providers_and_uses_anthropic_messages() {
|
||||
let home = agent_test_home("zcode-agent-config");
|
||||
let path = home.join(".zcode/v2/config.json");
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
let models = test_agent_models(&["gpt-test", "deepseek-test"]);
|
||||
let rendered = build_zcode_agent_config(
|
||||
Some(
|
||||
r#"{"locale":"zh-CN","provider":{"other":{"kind":"openai"},"cpa-gui":{"custom":"keep","options":{"timeout":30}}}}"#,
|
||||
),
|
||||
"http://127.0.0.1:8317",
|
||||
DEFAULT_API_KEY,
|
||||
"gpt-test",
|
||||
&models,
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(&path, &rendered).unwrap();
|
||||
let value: serde_json::Value = serde_json::from_str(&rendered).unwrap();
|
||||
|
||||
assert_eq!(value["locale"], "zh-CN");
|
||||
assert_eq!(value["provider"]["other"]["kind"], "openai");
|
||||
assert_eq!(
|
||||
value["provider"][MANAGED_AGENT_PROVIDER_ID]["custom"],
|
||||
"keep"
|
||||
);
|
||||
assert_eq!(
|
||||
value["provider"][MANAGED_AGENT_PROVIDER_ID]["options"]["timeout"],
|
||||
30
|
||||
);
|
||||
assert_eq!(
|
||||
value["provider"][MANAGED_AGENT_PROVIDER_ID]["kind"],
|
||||
"anthropic"
|
||||
);
|
||||
assert_eq!(
|
||||
value["provider"][MANAGED_AGENT_PROVIDER_ID]["apiFormat"],
|
||||
"anthropic-messages"
|
||||
);
|
||||
assert_eq!(
|
||||
value["provider"][MANAGED_AGENT_PROVIDER_ID]["options"]["baseURL"],
|
||||
"http://127.0.0.1:8317"
|
||||
);
|
||||
assert_eq!(value["model"], "cpa-gui/gpt-test");
|
||||
assert_eq!(
|
||||
inspect_zcode_agent_config(&path, 8317, DEFAULT_API_KEY).unwrap(),
|
||||
(true, Some("gpt-test".to_string()))
|
||||
);
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openclaw_agent_config_accepts_json5_and_preserves_unknown_fields() {
|
||||
let models = test_agent_models(&["gpt-test", "deepseek-test"]);
|
||||
|
||||
@@ -367,6 +367,63 @@ fn opencode_runtime_edits_survive_close_and_next_apply_owns_conflicts() {
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zcode_runtime_edits_survive_close_and_original_provider_is_restored() {
|
||||
let home = agent_test_home("zcode-runtime-edit-merge");
|
||||
let path = home.join(".zcode/v2/config.json");
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
fs::write(
|
||||
&path,
|
||||
r#"{"model":"other/original","provider":{"other":{"keep":"original"},"cpa-gui":{"name":"Original CPA","custom":"original"}},"keep":"root"}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let models = test_agent_models(&["gpt-one", "gpt-two"]);
|
||||
apply_agent_configuration(
|
||||
AgentClient::ZCode,
|
||||
&home,
|
||||
8317,
|
||||
DEFAULT_API_KEY,
|
||||
"gpt-one",
|
||||
&models,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut runtime: serde_json::Value =
|
||||
serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
|
||||
runtime["agentAdded"] = serde_json::json!({"enabled": true});
|
||||
runtime["provider"]["other"]["runtimeAdded"] = serde_json::json!(42);
|
||||
runtime["provider"][MANAGED_AGENT_PROVIDER_ID]["customAfterApply"] =
|
||||
serde_json::json!("keep-me");
|
||||
runtime["provider"][MANAGED_AGENT_PROVIDER_ID]["options"]["baseURL"] =
|
||||
serde_json::json!("https://agent-overwrite.invalid");
|
||||
fs::write(&path, serde_json::to_string_pretty(&runtime).unwrap()).unwrap();
|
||||
|
||||
restore_agent_session_configuration(AgentClient::ZCode, &home).unwrap();
|
||||
let closed: serde_json::Value =
|
||||
serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
|
||||
assert_eq!(closed["keep"], "root");
|
||||
assert_eq!(closed["model"], "other/original");
|
||||
assert_eq!(closed["agentAdded"]["enabled"], true);
|
||||
assert_eq!(closed["provider"]["other"]["runtimeAdded"], 42);
|
||||
assert_eq!(
|
||||
closed["provider"][MANAGED_AGENT_PROVIDER_ID]["name"],
|
||||
"Original CPA"
|
||||
);
|
||||
assert_eq!(
|
||||
closed["provider"][MANAGED_AGENT_PROVIDER_ID]["custom"],
|
||||
"original"
|
||||
);
|
||||
assert_eq!(
|
||||
closed["provider"][MANAGED_AGENT_PROVIDER_ID]["customAfterApply"],
|
||||
"keep-me"
|
||||
);
|
||||
assert!(closed["provider"][MANAGED_AGENT_PROVIDER_ID]
|
||||
.get("options")
|
||||
.is_none());
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_merge_preserves_runtime_fields_for_other_agent_formats() {
|
||||
let claude_original = r#"{"env":{"KEEP_ENV":"original"},"keep":"claude"}"#;
|
||||
|
||||
Reference in new issue
Block a user