fix(agents): speed up detection and sync ZCode default model
This commit is contained in:
1 parent
607c930d7b
commit
b312b904a2
9 files changed
+525
-96
No files matched your search
@@ -1,5 +1,13 @@
|
||||
use super::*;
|
||||
|
||||
const AGENT_STATUS_DETECTION_CONCURRENCY: usize = 4;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum AgentStatusDetectionTarget {
|
||||
Client(AgentClient),
|
||||
PiProvider,
|
||||
}
|
||||
|
||||
pub(crate) fn inspect_agent_config_statuses(
|
||||
app: &tauri::AppHandle,
|
||||
config: &GuiConfigFile,
|
||||
@@ -9,23 +17,63 @@ pub(crate) fn inspect_agent_config_statuses(
|
||||
.home_dir()
|
||||
.map_err(|error| format!("无法获取用户目录: {error}"))?;
|
||||
let api_key = effective_agent_api_key(config);
|
||||
let mut statuses = [
|
||||
AgentClient::ClaudeCode,
|
||||
AgentClient::ClaudeDesktop,
|
||||
AgentClient::Codex,
|
||||
AgentClient::OpenCode,
|
||||
AgentClient::OpenClaw,
|
||||
AgentClient::Hermes,
|
||||
AgentClient::DeepSeekHarness,
|
||||
AgentClient::ZCode,
|
||||
AgentClient::KimiCode,
|
||||
AgentClient::GrokBuild,
|
||||
]
|
||||
.into_iter()
|
||||
.map(|client| inspect_agent_config(client, &home, config.port, api_key))
|
||||
.collect::<Vec<_>>();
|
||||
statuses.push(inspect_pi_provider_status(&home, config.port, api_key));
|
||||
Ok(statuses)
|
||||
let targets = [
|
||||
AgentStatusDetectionTarget::Client(AgentClient::ClaudeCode),
|
||||
AgentStatusDetectionTarget::Client(AgentClient::ClaudeDesktop),
|
||||
AgentStatusDetectionTarget::Client(AgentClient::Codex),
|
||||
AgentStatusDetectionTarget::Client(AgentClient::OpenCode),
|
||||
AgentStatusDetectionTarget::Client(AgentClient::OpenClaw),
|
||||
AgentStatusDetectionTarget::Client(AgentClient::Hermes),
|
||||
AgentStatusDetectionTarget::Client(AgentClient::DeepSeekHarness),
|
||||
AgentStatusDetectionTarget::Client(AgentClient::ZCode),
|
||||
AgentStatusDetectionTarget::Client(AgentClient::KimiCode),
|
||||
AgentStatusDetectionTarget::Client(AgentClient::GrokBuild),
|
||||
AgentStatusDetectionTarget::PiProvider,
|
||||
];
|
||||
let queue = Mutex::new(targets.into_iter().enumerate());
|
||||
let results = Mutex::new(vec![None; targets.len()]);
|
||||
thread::scope(|scope| {
|
||||
let mut workers = Vec::with_capacity(AGENT_STATUS_DETECTION_CONCURRENCY);
|
||||
for _ in 0..AGENT_STATUS_DETECTION_CONCURRENCY {
|
||||
let queue = &queue;
|
||||
let results = &results;
|
||||
let home = &home;
|
||||
workers.push(scope.spawn(move || -> Result<(), String> {
|
||||
loop {
|
||||
let next = queue
|
||||
.lock()
|
||||
.map_err(|_| "智能体检测任务队列锁已损坏".to_string())?
|
||||
.next();
|
||||
let Some((index, target)) = next else {
|
||||
return Ok(());
|
||||
};
|
||||
let status = match target {
|
||||
AgentStatusDetectionTarget::Client(client) => {
|
||||
inspect_agent_config(client, home, config.port, api_key)
|
||||
}
|
||||
AgentStatusDetectionTarget::PiProvider => {
|
||||
inspect_pi_provider_status(home, config.port, api_key)
|
||||
}
|
||||
};
|
||||
results
|
||||
.lock()
|
||||
.map_err(|_| "智能体检测结果锁已损坏".to_string())?[index] = Some(status);
|
||||
}
|
||||
}));
|
||||
}
|
||||
for worker in workers {
|
||||
worker
|
||||
.join()
|
||||
.map_err(|_| "智能体检测工作线程异常退出".to_string())??;
|
||||
}
|
||||
Ok::<(), String>(())
|
||||
})?;
|
||||
results
|
||||
.into_inner()
|
||||
.map_err(|_| "智能体检测结果锁已损坏".to_string())?
|
||||
.into_iter()
|
||||
.map(|status| status.ok_or_else(|| "智能体检测结果不完整".to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_agent_config_status_cache(
|
||||
|
||||
@@ -215,16 +215,31 @@ 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)
|
||||
let app_before = read_optional_text(&paths[0])?;
|
||||
let cli_before = read_optional_text(&paths[1])?;
|
||||
let app_after =
|
||||
build_zcode_agent_config(app_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,
|
||||
}])
|
||||
let cli_after = build_zcode_cli_agent_config(
|
||||
cli_before.as_deref(),
|
||||
&root_base,
|
||||
api_key,
|
||||
model,
|
||||
models,
|
||||
)
|
||||
.or_else(|_| build_zcode_cli_agent_config(None, &root_base, api_key, model, models))?;
|
||||
Ok(vec![
|
||||
AgentFileUpdate {
|
||||
path: paths[0].clone(),
|
||||
after: app_after,
|
||||
},
|
||||
AgentFileUpdate {
|
||||
path: paths[1].clone(),
|
||||
after: cli_after,
|
||||
},
|
||||
])
|
||||
}
|
||||
AgentClient::KimiCode => {
|
||||
let before = read_optional_text(&paths[0])?;
|
||||
@@ -1031,35 +1046,59 @@ pub(crate) fn remove_opencode_managed_configuration(
|
||||
}
|
||||
|
||||
pub(crate) fn remove_zcode_managed_configuration(paths: &[PathBuf]) -> Result<Vec<String>, String> {
|
||||
let Some(path) = paths.first() else {
|
||||
if paths.is_empty() {
|
||||
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 mut changed_paths = Vec::new();
|
||||
for path in paths {
|
||||
let updated = update_agent_json_file(path, "ZCode 配置", |root| {
|
||||
let mut changed = false;
|
||||
let remove_model = if root
|
||||
.get("model")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_some_and(|model| model.starts_with(&prefix))
|
||||
{
|
||||
true
|
||||
} else if let Some(model) = root
|
||||
.get_mut("model")
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
{
|
||||
if model
|
||||
.get("main")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_some_and(|model| model.starts_with(&prefix))
|
||||
{
|
||||
model.remove("main");
|
||||
changed = true;
|
||||
}
|
||||
model.is_empty()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if remove_model {
|
||||
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
|
||||
})?;
|
||||
if updated {
|
||||
changed_paths.push(path_to_string(path));
|
||||
}
|
||||
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())
|
||||
}
|
||||
Ok(changed_paths)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_kimi_code_managed_configuration(
|
||||
@@ -2706,6 +2745,27 @@ pub(crate) fn build_zcode_agent_config(
|
||||
api_key: &str,
|
||||
model: &str,
|
||||
available_models: &[AgentModelOption],
|
||||
) -> Result<String, String> {
|
||||
build_zcode_config(existing, base_url, api_key, model, available_models, false)
|
||||
}
|
||||
|
||||
pub(crate) fn build_zcode_cli_agent_config(
|
||||
existing: Option<&str>,
|
||||
base_url: &str,
|
||||
api_key: &str,
|
||||
model: &str,
|
||||
available_models: &[AgentModelOption],
|
||||
) -> Result<String, String> {
|
||||
build_zcode_config(existing, base_url, api_key, model, available_models, true)
|
||||
}
|
||||
|
||||
fn build_zcode_config(
|
||||
existing: Option<&str>,
|
||||
base_url: &str,
|
||||
api_key: &str,
|
||||
model: &str,
|
||||
available_models: &[AgentModelOption],
|
||||
cli_config: bool,
|
||||
) -> Result<String, String> {
|
||||
let mut root = parse_agent_json_object(existing, "ZCode config.json")?;
|
||||
let providers = ensure_json_object_entry(&mut root, "provider");
|
||||
@@ -2739,10 +2799,13 @@ pub(crate) fn build_zcode_agent_config(
|
||||
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}")),
|
||||
);
|
||||
let model = format!("{MANAGED_AGENT_PROVIDER_ID}/{model}");
|
||||
if cli_config {
|
||||
ensure_json_object_entry(&mut root, "model")
|
||||
.insert("main".to_string(), serde_json::json!(model));
|
||||
} else {
|
||||
root.insert("model".to_string(), serde_json::json!(model));
|
||||
}
|
||||
render_agent_json(root, "ZCode 配置")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::*;
|
||||
|
||||
const AGENT_VERSION_PROBE_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const AGENT_VERSION_PROBE_POLL_INTERVAL: Duration = Duration::from_millis(20);
|
||||
|
||||
pub(crate) fn agent_config_paths(client: AgentClient, home: &Path) -> Vec<PathBuf> {
|
||||
match client {
|
||||
AgentClient::ClaudeCode => {
|
||||
@@ -32,7 +35,10 @@ 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)],
|
||||
AgentClient::ZCode => vec![
|
||||
home.join(".zcode/v2").join(ZCODE_CONFIG_FILE),
|
||||
home.join(".zcode/cli").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)],
|
||||
}
|
||||
@@ -968,8 +974,19 @@ 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)),
|
||||
AgentClient::ZCode => {
|
||||
if paths.len() != 2 {
|
||||
return Err("ZCode 配置路径数量无效".to_string());
|
||||
}
|
||||
let (app_configured, app_model) = inspect_zcode_agent_config(&paths[0], port, api_key)?;
|
||||
let (cli_configured, cli_model) = inspect_zcode_agent_config(&paths[1], port, api_key)?;
|
||||
let models_match = app_model.is_some() && app_model == cli_model;
|
||||
Ok((
|
||||
app_configured && cli_configured && models_match,
|
||||
cli_model.or(app_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)
|
||||
@@ -1094,20 +1111,29 @@ pub(crate) fn agent_has_managed_marker(
|
||||
}
|
||||
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)
|
||||
for path in paths {
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
let root = read_agent_json_or_empty(path, "ZCode 配置")?;
|
||||
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(|model| {
|
||||
model
|
||||
.as_str()
|
||||
.or_else(|| model.get("main").and_then(serde_json::Value::as_str))
|
||||
})
|
||||
.is_some_and(|model| model.starts_with(&prefix));
|
||||
if provider_exists && model_selected {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
AgentClient::KimiCode => inspect_managed_toml_model_marker(
|
||||
&paths[0],
|
||||
@@ -1322,11 +1348,12 @@ pub(crate) fn find_zcode_desktop_executable(home: &Path) -> Option<PathBuf> {
|
||||
pub(crate) fn read_macos_app_version(application: &Path) -> Option<String> {
|
||||
let info_plist = application.join("Contents/Info.plist");
|
||||
for key in ["CFBundleShortVersionString", "CFBundleVersion"] {
|
||||
let output = Command::new("/usr/bin/plutil")
|
||||
let mut command = Command::new("/usr/bin/plutil");
|
||||
command
|
||||
.args(["-extract", key, "raw", "-o", "-"])
|
||||
.arg(&info_plist)
|
||||
.output()
|
||||
.ok()?;
|
||||
.arg(&info_plist);
|
||||
let output =
|
||||
command_output_with_timeout(&mut command, AGENT_VERSION_PROBE_TIMEOUT).ok()??;
|
||||
if !output.status.success() {
|
||||
continue;
|
||||
}
|
||||
@@ -1420,7 +1447,7 @@ if ($package -and $package.Version) {
|
||||
&encoded_command,
|
||||
]);
|
||||
configure_background_command(&mut command);
|
||||
let output = command.output().ok()?;
|
||||
let output = command_output_with_timeout(&mut command, AGENT_VERSION_PROBE_TIMEOUT).ok()??;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
@@ -1456,7 +1483,7 @@ if ($package -and $package.Version) {
|
||||
&encoded_command,
|
||||
]);
|
||||
configure_background_command(&mut command);
|
||||
let output = command.output().ok()?;
|
||||
let output = command_output_with_timeout(&mut command, AGENT_VERSION_PROBE_TIMEOUT).ok()??;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
@@ -1543,7 +1570,7 @@ foreach ($shortcutFile in (Get-ChildItem -LiteralPath $shortcutRoots -Filter '*.
|
||||
&encoded_command,
|
||||
]);
|
||||
configure_background_command(&mut command);
|
||||
let output = command.output().ok()?;
|
||||
let output = command_output_with_timeout(&mut command, AGENT_VERSION_PROBE_TIMEOUT).ok()??;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
@@ -1596,7 +1623,7 @@ if ($version) {{
|
||||
&encoded_command,
|
||||
]);
|
||||
configure_background_command(&mut command);
|
||||
let output = command.output().ok()?;
|
||||
let output = command_output_with_timeout(&mut command, AGENT_VERSION_PROBE_TIMEOUT).ok()??;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
@@ -1642,7 +1669,9 @@ pub(crate) fn find_windows_codex_app_id_via_registry() -> Option<String> {
|
||||
let mut command = Command::new(windows_registry_executable());
|
||||
command.args(["query", PACKAGES_KEY, "/f", package_name, "/k", "/s"]);
|
||||
configure_background_command(&mut command);
|
||||
let Ok(output) = command.output() else {
|
||||
let Ok(Some(output)) =
|
||||
command_output_with_timeout(&mut command, AGENT_VERSION_PROBE_TIMEOUT)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if !output.status.success() {
|
||||
@@ -1914,6 +1943,74 @@ pub(crate) fn windows_batch_executable_argument(executable: &Path) -> String {
|
||||
format!("\"{}\"", path_to_string(executable))
|
||||
}
|
||||
|
||||
pub(crate) fn command_output_with_timeout(
|
||||
command: &mut Command,
|
||||
timeout: Duration,
|
||||
) -> io::Result<Option<std::process::Output>> {
|
||||
command
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
let mut child = command.spawn()?;
|
||||
let mut stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "无法读取智能体探测标准输出"))?;
|
||||
let mut stderr = child
|
||||
.stderr
|
||||
.take()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "无法读取智能体探测错误输出"))?;
|
||||
thread::scope(|scope| {
|
||||
let stdout_reader = scope.spawn(move || {
|
||||
let mut output = Vec::new();
|
||||
stdout.read_to_end(&mut output).map(|_| output)
|
||||
});
|
||||
let stderr_reader = scope.spawn(move || {
|
||||
let mut output = Vec::new();
|
||||
stderr.read_to_end(&mut output).map(|_| output)
|
||||
});
|
||||
let deadline = Instant::now() + timeout;
|
||||
let status = loop {
|
||||
if let Some(status) = child.try_wait()? {
|
||||
break Some(status);
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
terminate_agent_probe_process(&mut child);
|
||||
break None;
|
||||
}
|
||||
thread::sleep(AGENT_VERSION_PROBE_POLL_INTERVAL);
|
||||
};
|
||||
let stdout = stdout_reader
|
||||
.join()
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::Other, "智能体探测输出线程异常退出"))??;
|
||||
let stderr = stderr_reader
|
||||
.join()
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::Other, "智能体探测错误线程异常退出"))??;
|
||||
Ok(status.map(|status| std::process::Output {
|
||||
status,
|
||||
stdout,
|
||||
stderr,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
fn terminate_agent_probe_process(child: &mut Child) {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let process_id = child.id().to_string();
|
||||
let mut command = Command::new("taskkill");
|
||||
command
|
||||
.args(["/PID", &process_id, "/T", "/F"])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
configure_background_command(&mut command);
|
||||
let _ = command.status();
|
||||
}
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
|
||||
pub(crate) fn read_agent_version(path: &Path) -> Option<String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let mut command = windows_command_for_executable(path, false);
|
||||
@@ -1923,7 +2020,7 @@ pub(crate) fn read_agent_version(path: &Path) -> Option<String> {
|
||||
|
||||
command.arg("--version");
|
||||
configure_background_command(&mut command);
|
||||
let output = command.output().ok()?;
|
||||
let output = command_output_with_timeout(&mut command, AGENT_VERSION_PROBE_TIMEOUT).ok()??;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
@@ -2471,7 +2568,11 @@ pub(crate) fn inspect_zcode_agent_config(
|
||||
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
|
||||
let model = root
|
||||
.get("model")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.and_then(|model| {
|
||||
model
|
||||
.as_str()
|
||||
.or_else(|| model.get("main").and_then(serde_json::Value::as_str))
|
||||
})
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.and_then(|value| value.strip_prefix(&prefix))
|
||||
|
||||
@@ -189,7 +189,11 @@ pub(crate) fn validate_agent_applied_state(
|
||||
.map(|file| file.path.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let valid_paths = state_paths == expected_paths
|
||||
|| (client == AgentClient::Codex && state_paths.as_slice() == paths);
|
||||
|| (client == AgentClient::Codex && state_paths.as_slice() == paths)
|
||||
|| (client == AgentClient::ZCode
|
||||
&& paths
|
||||
.first()
|
||||
.is_some_and(|path| state_paths == [path.clone()]));
|
||||
if !valid_paths {
|
||||
return Err("智能体应用状态文件数量或路径不匹配".to_string());
|
||||
}
|
||||
@@ -385,7 +389,11 @@ pub(crate) fn validate_agent_record(
|
||||
.map(|file| file.path.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let valid_paths = record_paths.as_slice() == paths
|
||||
|| (client == AgentClient::Codex && record_paths == expected_paths);
|
||||
|| (client == AgentClient::Codex && record_paths == expected_paths)
|
||||
|| (client == AgentClient::ZCode
|
||||
&& paths
|
||||
.first()
|
||||
.is_some_and(|path| record_paths == [path.clone()]));
|
||||
if !valid_paths {
|
||||
return Err("智能体备份状态文件数量或路径不匹配".to_string());
|
||||
}
|
||||
@@ -728,9 +736,10 @@ 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,
|
||||
)?]),
|
||||
AgentClient::ZCode => Ok(vec![
|
||||
build_zcode_agent_config(None, &root_base, api_key, model, models)?,
|
||||
build_zcode_cli_agent_config(None, &root_base, api_key, model, models)?,
|
||||
]),
|
||||
AgentClient::KimiCode => Ok(vec![build_kimi_code_agent_config(
|
||||
None,
|
||||
&openai_base,
|
||||
@@ -1430,14 +1439,36 @@ pub(crate) fn commit_agent_configuration(
|
||||
.collect::<Result<Vec<_>, String>>()?;
|
||||
snapshots.push((state_path.clone(), previous_state));
|
||||
|
||||
let created_session_backups = existing_state
|
||||
.as_ref()
|
||||
.is_none_or(|state| state.backup_files.is_empty());
|
||||
let mut created_backup_paths = Vec::new();
|
||||
let backup_files =
|
||||
if let Some(state) = existing_state.filter(|state| !state.backup_files.is_empty()) {
|
||||
state.backup_files
|
||||
let mut backup_files = state.backup_files;
|
||||
let missing_updates = updates
|
||||
.iter()
|
||||
.filter(|update| !backup_files.iter().any(|file| file.path == update.path))
|
||||
.map(|update| AgentFileUpdate {
|
||||
path: update.path.clone(),
|
||||
after: update.after.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let added_backups = prepare_agent_session_backups(client, &missing_updates)?;
|
||||
created_backup_paths.extend(
|
||||
added_backups
|
||||
.iter()
|
||||
.filter(|file| file.existed_before)
|
||||
.map(|file| file.backup_path.clone()),
|
||||
);
|
||||
backup_files.extend(added_backups);
|
||||
backup_files
|
||||
} else {
|
||||
prepare_agent_session_backups(client, updates)?
|
||||
let backup_files = prepare_agent_session_backups(client, updates)?;
|
||||
created_backup_paths.extend(
|
||||
backup_files
|
||||
.iter()
|
||||
.filter(|file| file.existed_before)
|
||||
.map(|file| file.backup_path.clone()),
|
||||
);
|
||||
backup_files
|
||||
};
|
||||
|
||||
let transaction = (|| -> Result<Vec<String>, String> {
|
||||
@@ -1484,11 +1515,9 @@ pub(crate) fn commit_agent_configuration(
|
||||
}
|
||||
Err(error) => {
|
||||
let rollback = restore_agent_snapshots_direct(&snapshots);
|
||||
if created_session_backups {
|
||||
for backup in &backup_files {
|
||||
if backup.existed_before && backup.backup_path.is_file() {
|
||||
let _ = fs::remove_file(&backup.backup_path);
|
||||
}
|
||||
for backup_path in created_backup_paths {
|
||||
if backup_path.is_file() {
|
||||
let _ = fs::remove_file(backup_path);
|
||||
}
|
||||
}
|
||||
match rollback {
|
||||
|
||||
@@ -684,6 +684,55 @@ fn zcode_agent_config_preserves_other_providers_and_uses_anthropic_messages() {
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zcode_cli_config_sets_main_model_and_both_configs_must_match() {
|
||||
let home = agent_test_home("zcode-cli-default-model");
|
||||
let paths = agent_config_paths(AgentClient::ZCode, &home);
|
||||
fs::create_dir_all(paths[0].parent().unwrap()).unwrap();
|
||||
fs::create_dir_all(paths[1].parent().unwrap()).unwrap();
|
||||
let models = test_agent_models(&["gpt-test", "deepseek-test"]);
|
||||
let app_config = build_zcode_agent_config(
|
||||
None,
|
||||
"http://127.0.0.1:8317",
|
||||
DEFAULT_API_KEY,
|
||||
"gpt-test",
|
||||
&models,
|
||||
)
|
||||
.unwrap();
|
||||
let cli_config = build_zcode_cli_agent_config(
|
||||
Some(r#"{"model":{"lite":"other/lite"},"plugins":{"keep":true}}"#),
|
||||
"http://127.0.0.1:8317",
|
||||
DEFAULT_API_KEY,
|
||||
"gpt-test",
|
||||
&models,
|
||||
)
|
||||
.unwrap();
|
||||
let cli_value: serde_json::Value = serde_json::from_str(&cli_config).unwrap();
|
||||
|
||||
assert_eq!(cli_value["model"]["main"], "cpa-gui/gpt-test");
|
||||
assert_eq!(cli_value["model"]["lite"], "other/lite");
|
||||
assert_eq!(cli_value["plugins"]["keep"], true);
|
||||
fs::write(&paths[0], app_config).unwrap();
|
||||
fs::write(&paths[1], &cli_config).unwrap();
|
||||
assert_eq!(
|
||||
inspect_agent_managed_config(AgentClient::ZCode, &paths, 8317, DEFAULT_API_KEY).unwrap(),
|
||||
(true, Some("gpt-test".to_string()), false)
|
||||
);
|
||||
|
||||
let mut mismatched = cli_value;
|
||||
mismatched["model"]["main"] = serde_json::json!("cpa-gui/deepseek-test");
|
||||
fs::write(
|
||||
&paths[1],
|
||||
serde_json::to_string_pretty(&mismatched).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
inspect_agent_managed_config(AgentClient::ZCode, &paths, 8317, DEFAULT_API_KEY).unwrap(),
|
||||
(false, Some("deepseek-test".to_string()), false)
|
||||
);
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zcode_inspection_requires_the_default_model_to_belong_to_the_managed_provider() {
|
||||
let home = agent_test_home("zcode-default-model-inspection");
|
||||
@@ -1583,6 +1632,62 @@ fn detected_agent_version_requires_a_real_version_value() {
|
||||
assert!(normalize_detected_agent_version(&"1".repeat(257)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_probe_command_captures_output_before_timeout() {
|
||||
#[cfg(target_os = "windows")]
|
||||
let mut command = {
|
||||
let mut command = Command::new(windows_command_processor());
|
||||
command.args(["/D", "/C", "echo agent 1.2.3"]);
|
||||
configure_background_command(&mut command);
|
||||
command
|
||||
};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let mut command = {
|
||||
let mut command = Command::new("sh");
|
||||
command.args(["-c", "printf 'agent 1.2.3\\n'"]);
|
||||
command
|
||||
};
|
||||
|
||||
let output = command_output_with_timeout(&mut command, Duration::from_secs(2))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(output.status.success());
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&output.stdout).trim(),
|
||||
"agent 1.2.3"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_probe_command_stops_at_timeout() {
|
||||
#[cfg(target_os = "windows")]
|
||||
let mut command = {
|
||||
let mut command = Command::new(windows_powershell_executable());
|
||||
command.args([
|
||||
"-NoLogo",
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
"Start-Sleep -Seconds 5",
|
||||
]);
|
||||
configure_background_command(&mut command);
|
||||
command
|
||||
};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let mut command = {
|
||||
let mut command = Command::new("sh");
|
||||
command.args(["-c", "sleep 5"]);
|
||||
command
|
||||
};
|
||||
|
||||
let started = Instant::now();
|
||||
let output = command_output_with_timeout(&mut command, Duration::from_millis(100)).unwrap();
|
||||
|
||||
assert!(output.is_none());
|
||||
assert!(started.elapsed() < Duration::from_secs(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_model_list_is_empty_when_cpa_has_no_writable_models() {
|
||||
let prepared = prepare_codex_agent_models(&[]).unwrap();
|
||||
|
||||
@@ -424,6 +424,71 @@ fn zcode_runtime_edits_survive_close_and_original_provider_is_restored() {
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zcode_legacy_single_file_state_upgrades_and_restores_cli_config() {
|
||||
let home = agent_test_home("zcode-legacy-state-upgrade");
|
||||
let paths = agent_config_paths(AgentClient::ZCode, &home);
|
||||
fs::create_dir_all(paths[0].parent().unwrap()).unwrap();
|
||||
fs::create_dir_all(paths[1].parent().unwrap()).unwrap();
|
||||
let original_app = br#"{"model":"other/original","provider":{"other":{"keep":true}}}"#;
|
||||
let original_cli =
|
||||
br#"{"model":{"main":"other/original","lite":"other/lite"},"plugins":{"keep":true}}"#;
|
||||
let models = test_agent_models(&["gpt-one", "gpt-two"]);
|
||||
let managed_app = build_zcode_agent_config(
|
||||
Some(std::str::from_utf8(original_app).unwrap()),
|
||||
"http://127.0.0.1:8317",
|
||||
DEFAULT_API_KEY,
|
||||
"gpt-one",
|
||||
&models,
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(&paths[0], managed_app).unwrap();
|
||||
fs::write(&paths[1], original_cli).unwrap();
|
||||
let app_backup = dated_agent_backup_path(&paths[0]).unwrap();
|
||||
fs::write(&app_backup, original_app).unwrap();
|
||||
let state_path = agent_state_path(&paths).unwrap();
|
||||
write_agent_applied_state(
|
||||
&state_path,
|
||||
&AgentAppliedState {
|
||||
version: AGENT_APPLIED_STATE_VERSION,
|
||||
client: AgentClient::ZCode.id().to_string(),
|
||||
model: "gpt-one".to_string(),
|
||||
claude_desktop_model_mappings: None,
|
||||
backup_files: vec![AgentAppliedBackupFile {
|
||||
path: paths[0].clone(),
|
||||
backup_path: app_backup,
|
||||
existed_before: true,
|
||||
}],
|
||||
updated_at_unix: 1,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
apply_agent_configuration(
|
||||
AgentClient::ZCode,
|
||||
&home,
|
||||
8317,
|
||||
DEFAULT_API_KEY,
|
||||
"gpt-two",
|
||||
&models,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let upgraded = load_agent_applied_state(AgentClient::ZCode, &home)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(upgraded.backup_files.len(), 2);
|
||||
let cli_value: serde_json::Value =
|
||||
serde_json::from_str(&fs::read_to_string(&paths[1]).unwrap()).unwrap();
|
||||
assert_eq!(cli_value["model"]["main"], "cpa-gui/gpt-two");
|
||||
|
||||
restore_agent_session_configuration(AgentClient::ZCode, &home).unwrap();
|
||||
assert_eq!(fs::read(&paths[0]).unwrap(), original_app);
|
||||
assert_eq!(fs::read(&paths[1]).unwrap(), original_cli);
|
||||
assert!(!state_path.exists());
|
||||
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"}"#;
|
||||
|
||||
@@ -885,6 +885,7 @@ export function AgentsPage() {
|
||||
const configurationAction = resolveAgentConfigurationAction({
|
||||
client: selected,
|
||||
modificationState: activeStatus?.modificationState ?? 'unconfigured',
|
||||
configurationSynchronized: Boolean(activeStatus?.configured),
|
||||
selectedModel,
|
||||
appliedModel,
|
||||
oauthConfiguration,
|
||||
|
||||
@@ -32,6 +32,7 @@ export type AgentModelMappings = {
|
||||
type ResolveAgentConfigurationActionOptions = {
|
||||
client: AgentConfigurationClientId;
|
||||
modificationState: AgentConfigurationModificationState;
|
||||
configurationSynchronized?: boolean;
|
||||
selectedModel: string;
|
||||
appliedModel: string;
|
||||
oauthConfiguration: boolean;
|
||||
@@ -82,6 +83,7 @@ export const resolveAgentModelMappingsDraftSourceForClient = <T>(
|
||||
export function resolveAgentConfigurationAction({
|
||||
client,
|
||||
modificationState,
|
||||
configurationSynchronized = true,
|
||||
selectedModel,
|
||||
appliedModel,
|
||||
oauthConfiguration,
|
||||
@@ -91,6 +93,7 @@ export function resolveAgentConfigurationAction({
|
||||
}: ResolveAgentConfigurationActionOptions): AgentConfigurationAction {
|
||||
if (modificationState !== 'applied') return 'apply';
|
||||
if (client === 'pi') return 'close';
|
||||
if (client === 'zcode' && !configurationSynchronized) return 'update';
|
||||
|
||||
const modelChanged = client === 'claude-code' || client === 'claude-desktop'
|
||||
? !sameAgentModelMappings(modelMappings, appliedModelMappings)
|
||||
|
||||
@@ -161,6 +161,20 @@ describe('agent configuration update action', () => {
|
||||
expect(appliedConfiguration(client, ' MODEL-A ')).toBe('close');
|
||||
});
|
||||
|
||||
test('ZCode updates an applied legacy configuration even when the model is unchanged', () => {
|
||||
expect(resolveAgentConfigurationAction({
|
||||
client: 'zcode',
|
||||
modificationState: 'applied',
|
||||
configurationSynchronized: false,
|
||||
selectedModel: 'model-a',
|
||||
appliedModel: 'model-a',
|
||||
oauthConfiguration: false,
|
||||
appliedOauthConfiguration: false,
|
||||
modelMappings: { opus: '', sonnet: '', haiku: '' },
|
||||
appliedModelMappings: { opus: '', sonnet: '', haiku: '' },
|
||||
})).toBe('update');
|
||||
});
|
||||
|
||||
test('Codex also updates when only OAuth configuration changes', () => {
|
||||
expect(resolveAgentConfigurationAction({
|
||||
client: 'codex',
|
||||
|
||||
Reference in new issue
Block a user