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
@@ -98,6 +98,7 @@ OpenAI、Claude、Gemini、およびその他の互換形式の間で変換で
|
||||
- OpenClaw
|
||||
- Hermes Agent
|
||||
- Pi(CLIProxyAPI provider 拡張機能)
|
||||
- ZCode
|
||||
|
||||
対応クライアントでは、利用可能なモデルカタログの同期、デフォルトモデルの選択、管理設定を適用する前の
|
||||
元設定のバックアップ、以前の設定への復元、利用可能なデスクトップまたは CLI エントリーポイントの起動ができます。
|
||||
|
||||
@@ -99,6 +99,7 @@ proxy. Supported clients include:
|
||||
- OpenClaw
|
||||
- Hermes Agent
|
||||
- Pi (with the CLIProxyAPI provider extension)
|
||||
- ZCode
|
||||
|
||||
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
|
||||
|
||||
@@ -95,6 +95,7 @@ API 接入页面按照协议或 Provider 管理上游 API 凭证和服务地址
|
||||
- OpenClaw
|
||||
- Hermes Agent
|
||||
- Pi(通过 CLIProxyAPI provider 插件)
|
||||
- ZCode
|
||||
|
||||
对于受支持的客户端,软件可以同步可用模型目录、选择默认模型、在应用托管配置前备份原始配置,
|
||||
以及恢复之前的配置。
|
||||
|
||||
@@ -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"}"#;
|
||||
|
||||
@@ -466,6 +466,7 @@ export const jaOverrides = {
|
||||
'agents.description.openclaw': 'CPA モデルプロバイダーと既定モデルを使用',
|
||||
'agents.description.hermes': 'CPA カスタムプロバイダーを使用',
|
||||
'agents.description.deepseekHarness': 'Harness のネイティブ DeepSeek 設定と分離された OpenAI 互換ルートで CPA を使用',
|
||||
'agents.description.zcode': 'CPA の Anthropic Messages API と動的モデルカタログを使用',
|
||||
'agents.description.pi': 'CLIProxyAPI プロバイダーを使用して Pi CLI のモデルを動的に読み込みます',
|
||||
'agents.list.detecting': '検出中',
|
||||
'agents.list.unsupported': '現在のプラットフォームでは非対応',
|
||||
@@ -617,6 +618,7 @@ export const jaOverrides = {
|
||||
'agents.modify.default': 'デフォルト設定',
|
||||
'agents.modify.clear': '設定を消去',
|
||||
'agents.modify.invalid': '現在の設定を解析できません。「設定変更を適用」は最小の有効な設定を再構築し、「デフォルト設定」はファイル全体をリセットします。',
|
||||
'agents.modify.zcodeRestart': '設定の適用または終了後、モデルプロバイダーを再読み込みするため ZCode を完全終了して再起動してください。',
|
||||
'agents.pi.installTitle': 'Pi CLIProxyAPI プロバイダー',
|
||||
'agents.pi.installDescription': 'プラグインをインストールまたは更新し、CPA 認証情報、既定のプロバイダー、既定のモデルを修復します',
|
||||
'agents.pi.install': 'Pi プラグインをインストール',
|
||||
|
||||
@@ -466,6 +466,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'agents.description.openclaw': 'Uses the CPA model provider and default model',
|
||||
'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.pi': 'Loads models dynamically in Pi through the CLIProxyAPI provider',
|
||||
'agents.list.detecting': 'Detecting',
|
||||
'agents.list.unsupported': 'Unsupported on this platform',
|
||||
@@ -617,6 +618,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'agents.modify.default': 'Default Configuration',
|
||||
'agents.modify.clear': 'Clear Configuration',
|
||||
'agents.modify.invalid': 'The current configuration cannot be parsed. Apply Configuration Changes rebuilds a minimal valid configuration; Default Configuration resets the whole file.',
|
||||
'agents.modify.zcodeRestart': 'Fully quit and restart ZCode after applying or closing the configuration so it reloads model providers.',
|
||||
'agents.pi.installTitle': 'Pi CLIProxyAPI provider',
|
||||
'agents.pi.installDescription': 'Install or update the plugin and repair CPA credentials, the default provider, and the default model.',
|
||||
'agents.pi.install': 'Install Pi Plugin',
|
||||
|
||||
@@ -464,6 +464,7 @@ export const zhCN = {
|
||||
'agents.description.openclaw': '使用 CPA 模型供应商和默认模型',
|
||||
'agents.description.hermes': '使用 CPA custom provider',
|
||||
'agents.description.deepseekHarness': '通过通用 OpenAI 兼容路由使用 CPA,并与 Harness 原生 DeepSeek 配置隔离',
|
||||
'agents.description.zcode': '使用 CPA 的 Anthropic Messages 接口和动态模型目录',
|
||||
'agents.description.pi': '使用 Pi CLI 和 CLIProxyAPI provider 动态加载模型',
|
||||
'agents.list.detecting': '正在检测',
|
||||
'agents.list.unsupported': '当前平台不支持',
|
||||
@@ -615,6 +616,7 @@ export const zhCN = {
|
||||
'agents.modify.default': '默认配置',
|
||||
'agents.modify.clear': '清空配置',
|
||||
'agents.modify.invalid': '当前配置无法解析。点击“应用配置修改”会重建最小合法配置;“默认配置”会重置整个文件。',
|
||||
'agents.modify.zcodeRestart': '应用或关闭配置后,请完全退出并重新启动 ZCode 以加载模型供应商。',
|
||||
'agents.pi.installTitle': 'Pi CLIProxyAPI provider',
|
||||
'agents.pi.installDescription': '安装或更新插件,并修复 CPA 凭据、默认 provider 与默认模型',
|
||||
'agents.pi.install': '安装 Pi 插件',
|
||||
|
||||
@@ -56,6 +56,7 @@ type AgentClientId =
|
||||
| 'openclaw'
|
||||
| 'hermes'
|
||||
| 'deepseek-harness'
|
||||
| 'zcode'
|
||||
| 'pi';
|
||||
|
||||
type ClaudeModelMappingClientId = 'claude-code' | 'claude-desktop';
|
||||
@@ -170,7 +171,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.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.pi';
|
||||
};
|
||||
|
||||
type AgentSubpageId = 'core' | 'sessions';
|
||||
@@ -224,6 +225,12 @@ const agentDefinitions: AgentDefinition[] = [
|
||||
icon: deepseekIcon,
|
||||
descriptionKey: 'agents.description.deepseekHarness',
|
||||
},
|
||||
{
|
||||
id: 'zcode',
|
||||
name: 'ZCode',
|
||||
Icon: Bot,
|
||||
descriptionKey: 'agents.description.zcode',
|
||||
},
|
||||
{
|
||||
id: 'pi',
|
||||
name: 'Pi',
|
||||
@@ -888,7 +895,9 @@ export function AgentsPage() {
|
||||
: t('agents.model.firstSelection', { count: models.length }));
|
||||
const modificationDescription = activeStatus?.modificationState === 'invalid'
|
||||
? t('agents.modify.invalid')
|
||||
: '';
|
||||
: selected === 'zcode'
|
||||
? t('agents.modify.zcodeRestart')
|
||||
: '';
|
||||
const refreshModels = () => {
|
||||
void loadModels(selected);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ export type AgentConfigurationClientId =
|
||||
| 'openclaw'
|
||||
| 'hermes'
|
||||
| 'deepseek-harness'
|
||||
| 'zcode'
|
||||
| 'pi';
|
||||
|
||||
export type AgentConfigurationModificationState = 'unconfigured' | 'applied' | 'invalid';
|
||||
|
||||
@@ -153,6 +153,7 @@ describe('agent configuration update action', () => {
|
||||
'opencode',
|
||||
'openclaw',
|
||||
'hermes',
|
||||
'zcode',
|
||||
])('%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